refactor(manifest): convert manifests to array

if the manifests are arrays, we can speed things up by indexing separately and
depend upon the order of the dictionary.

feat(manifest): add indexer

to speed things, up we're now indexing manifest items

feat(manifest): add publishing order optimizer

since we shouldn't be trusting the order of the provided pages manifest, we've
implemented sorting, so that the ancestry of pages is reflected in the order of
publishing.
This commit is contained in:
Rodweil, Theodor 2023-08-06 04:08:03 +02:00
parent c3aa057bfc
commit 3882b1089e
No known key found for this signature in database
GPG key ID: F8BC1B0EB1F9CCF5
3 changed files with 553 additions and 23 deletions

View file

@ -5,6 +5,7 @@ BeforeAll {
Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force
} }
AfterAll { AfterAll {
} }
@ -19,7 +20,7 @@ Describe 'Get-Manifest' `
InModuleScope Manifest ` InModuleScope Manifest `
{ {
Mock Get-Content { Mock Get-Content {
return '{"pages":{}, "attachments": {}}' return '{"pages":[], "attachments": []}'
} }
#mocking Get-Content, therefore file name can be bogus #mocking Get-Content, therefore file name can be bogus
@ -32,7 +33,7 @@ Describe 'Get-Manifest' `
InModuleScope Manifest ` InModuleScope Manifest `
{ {
Mock Get-Content { Mock Get-Content {
return '{"pagges":{}, "attsdachments": {}}' return '{"pagges":[], "attsdachments": []}'
} }
#mocking Get-Content, therefore file name can be bogus #mocking Get-Content, therefore file name can be bogus
@ -52,8 +53,8 @@ Describe 'Set-Manifest' `
InModuleScope Manifest ` InModuleScope Manifest `
{ {
$mockManifest = @{ $mockManifest = @{
'pages' = @{} 'pages' = @()
'attachments' = @{} 'attachments' = @()
} }
Mock Set-Content { Mock Set-Content {
@ -76,8 +77,8 @@ Describe 'Set-Manifest' `
InModuleScope Manifest ` InModuleScope Manifest `
{ {
$mockManifest = @{ $mockManifest = @{
'pagges' = @{} 'pagges' = @()
'attachments' = @{} 'attachments' = @()
} }
#mocking Get-Content, therefore file name can be bogus #mocking Get-Content, therefore file name can be bogus
@ -97,8 +98,8 @@ Describe 'Set-Manifest' `
InModuleScope Manifest ` InModuleScope Manifest `
{ {
$mockManifest = @{ $mockManifest = @{
'pages' = @{} 'pages' = @()
'attachments' = @{} 'attachments' = @()
} }
Mock Set-Content { Mock Set-Content {
@ -128,8 +129,8 @@ Describe 'Set-Manifest' `
InModuleScope Manifest ` InModuleScope Manifest `
{ {
$mockManifest = @{ $mockManifest = @{
'pages' = @{} 'pages' = @()
'attachments' = @{} 'attachments' = @()
} }
Mock Set-Content { Mock Set-Content {
@ -155,3 +156,260 @@ Describe 'Set-Manifest' `
} }
} }
} }
Describe 'New-PagesManifestIndex' `
{
Context 'default' `
{
BeforeEach `
{
$mockManifest = @(
@{
'Title' = 'foobar0'
},
@{
'Title' = 'foobar1'
}
)
}
It 'from pipeline' `
{
$index = New-PagesManifestIndex -Manifest $mockManifest
$index.foobar0 | Should -Be 0
$index.foobar1 | Should -Be 1
}
It 'from pipeline' `
{
$index = ,$mockManifest | New-PagesManifestIndex
$index.foobar0 | Should -Be 0
$index.foobar1 | Should -Be 1
}
}
}
Describe 'New-AttachmentsManifestIndex' `
{
Context 'default' `
{
BeforeEach `
{
$mockManifest = @(
@{
'ContainerPageTitle' = 'foobar0'
'Name' = 'attachment0'
},
@{
'ContainerPageTitle' = 'foobar1'
'Name' = 'attachment1'
}
)
}
It 'from parameter' `
{
$index = New-AttachmentsManifestIndex -Manifest $mockManifest
$index."foobar0:attachment0" | Should -Be 0
$index."foobar1:attachment1" | Should -Be 1
}
It 'from pipeline' `
{
$index = ,$mockManifest | New-AttachmentsManifestIndex
$index."foobar0:attachment0" | Should -Be 0
$index."foobar1:attachment1" | Should -Be 1
}
}
}
Describe 'Get-AncestralPageGenerationCache' `
{
Context 'default' `
{
BeforeEach `
{
$mockManifest = @(
@{
'Title' = 'foobar0'
},
@{
'Title' = 'foobar1'
'AncestorTitle' = 'foobar0'
}
)
$mockIndex = @{
'foobar0' = 0
'foobar1' = 1
}
}
It 'selects single operation by title' `
{
$result = New-AncestralPageGenerationCache `
-Title 'foobar0' `
-Manifest $mockManifest `
-Index $mockIndex
$result.Count | Should -Be 1
$result.foobar0 | Should -Be 0
}
It 'automatically builds index' `
{
$result = New-AncestralPageGenerationCache `
-Title 'foobar0' `
-Manifest $mockManifest `
$result.Count | Should -Be 1
$result.foobar0 | Should -Be 0
}
It 'accepts a pipeline' `
{
$result = New-AncestralPageGenerationCache `
-Manifest $mockManifest `
-Index $mockIndex
$result.Count | Should -Be 2
$result.foobar0 | Should -Be 0
$result.foobar1 | Should -Be 1
}
}
Context 'more complex' `
{
BeforeEach `
{
$mockManifest = @(
@{
'Title' = 'foobar0'
},
@{
'Title' = 'foobar4'
'AncestorTitle' = 'foobar3'
},
@{
'Title' = 'foobar1'
'AncestorTitle' = 'foobar0'
},
@{
'Title' = 'foobar3'
'AncestorTitle' = 'foobar2'
},
@{
'Title' = 'foobar2'
'AncestorTitle' = 'foobar1'
}
)
$mockIndex = @{
'foobar0' = 0
'foobar1' = 2
'foobar2' = 4
'foobar3' = 3
'foobar4' = 1
}
}
It 'uses index' `
{
$result = New-AncestralPageGenerationCache `
-Manifest $mockManifest `
-Index $mockIndex
$result.foobar0 | Should -Be 0
$result.foobar1 | Should -Be 1
$result.foobar2 | Should -Be 2
$result.foobar3 | Should -Be 3
$result.foobar4 | Should -Be 4
}
}
}
Describe 'Optimize-PagesManifest' `
{
Context 'default' `
{
BeforeEach `
{
$mockManifest = @(
@{
'Title' = 'foobar0'
},
@{
'Title' = 'foobar4'
'AncestorTitle' = 'foobar3'
},
@{
'Title' = 'foobar1'
'AncestorTitle' = 'foobar0'
},
@{
'Title' = 'foobar3'
'AncestorTitle' = 'foobar2'
},
@{
'Title' = 'foobar5'
'AncestorTitle' = 'foobar2'
},
@{
'Title' = 'foobar2'
'AncestorTitle' = 'foobar1'
}
)
$mockGenerationCache = @{
'foobar0' = 0
'foobar1' = 1
'foobar2' = 2
'foobar3' = 3
'foobar4' = 4
'foobar5' = 3
}
}
It 'from parameter' `
{
Optimize-PagesManifest `
-Manifest $mockManifest `
-Lo 0 `
-Hi ($mockManifest.Count - 1) `
-GenerationCache $mockGenerationCache
$mockManifest[0].Title | Should -Be 'foobar0'
$mockManifest[1].Title | Should -Be 'foobar1'
$mockManifest[2].Title | Should -Be 'foobar2'
$mockManifest[3].Title | Should -Be 'foobar5'
$mockManifest[4].Title | Should -Be 'foobar3'
$mockManifest[5].Title | Should -Be 'foobar4'
}
}
}

View file

@ -89,3 +89,270 @@ function Set-Manifest
Set-Content -Path $File -Value $raw Set-Content -Path $File -Value $raw
} }
} }
function New-AncestralPageGenerationCache {
<#
.SYNOPSIS
Calculate the numeric ancestral generation of a page
.DESCRIPTION
The Get-AncestralPageGeneration calculates a numeric ancestral
generation of a page, which is used for sorting.
The index required as input can be retrieved through the
New-PagesManifestIndex function.
.EXAMPLES
$generation = Get-AncestralPageGeneration `
-Title 'foobar4' `
-Manifest @() `
-Index @{}
#>
Param(
# Pages manifest
[Parameter(Mandatory)] [Array] $Manifest,
# Title of page to calculate generation of
[Parameter()] [String] $Title,
# Index for lookup of page metadata manifest item position
[Parameter()] [Collections.Hashtable] $Index
)
Begin
{
$cache = @{}
If (-Not $Index)
{
Write-Debug "rebuilding index"
$Index = ,$Manifest | New-PagesManifestIndex
}
}
Process
{
ForEach ($pageMeta in $Manifest)
{
$generation = 0
$pageMeta = $Title ? $Manifest[$Index.$Title] : $pageMeta
$ancestor = $pageMeta.AncestorTitle
$pageMeta_ = $pageMeta
While ($ancestor)
{
$generation += 1
$pageMeta_ = $Manifest[$Index."$($pageMeta_.AncestorTitle)"]
$ancestor = $pageMeta_.AncestorTitle
}
$cache[$pageMeta.Title] = $generation
if ($Title) {Break}
}
}
End {$cache}
}
function Optimize-PagesManifest
{
<#
.SYNOPSIS
Sort Pages Manifest in accordance with the pages ancestry
.DESCRIPTION
The Optimize-PagesManifest function sorts a Pages manifest in
accordance with the pages ancestry. This makes sure that an ancestor
is already published, before its descendant is published.
The sorting is done with a quick-sort algorithm, using the Hoare
partitioning scheme.
Older/lower ancestral generations take precedence over
higher/younger ones. Syblings within a generation are treated
as LIFO, where the youngest (last) has precedence over the oldest
(fist).
.EXAMPLE
$manifest Optimize-PagesManifest -Manifest ,@()
or
$manifest = ,@() | Optimize-PagesManifest
.NOTES
whichever system generates the manifest should already output the
array in the correct order, however it is not wise to depend upon
this.
#>
Param(
[Parameter(Mandatory)] [Array] $Manifest,
[Parameter(Mandatory)] [Int] $Lo,
[Parameter(Mandatory)] [Int] $Hi,
# cache for storing the numeric ancestral generation of pages
[Parameter(Mandatory)] [Collections.Hashtable] $GenerationCache
)
Process
{
$pivotPageMeta = $Manifest[($Lo + $Hi) / 2]
$pivot = $generationCache[$pivotPageMeta.Title]
# left index
$i = $Lo
# right index
$j = $Hi
While($i -le $j)
{
# Move the left index to the right at least once and while
# the element at the left index is less than the pivot
While (
$generationCache."$($Manifest[$i].Title)" -lt $pivot `
-And `
$i -lt $Hi
)
{
$i += 1
}
# Move the right index to the left at least once and while
# element at the right index is greater than the pivot
While (
$generationCache."$($Manifest[$j].Title)" -gt $pivot `
-And `
$j -gt $Lo
)
{
$j -= 1
}
If ($i -le $j)
{
$tmp = $Manifest[$i]
$Manifest[$i] = $Manifest[$j]
$Manifest[$j] = $tmp
$i += 1
$j -= 1
}
If ($Lo -lt $j)
{
Optimize-PagesManifest `
-Manifest $Manifest `
-Lo $Lo`
-Hi $j `
-GenerationCache $GenerationCache
}
If ($i -lt $Hi)
{
Optimize-PagesManifest `
-Manifest $Manifest `
-Lo $i `
-Hi $Hi `
-GenerationCache $GenerationCache
}
}
}
}
function New-PagesManifestIndex
{
<#
.SYNOPSIS
Create an index of pages from a manifest
.DESCRIPTION
The New-PageIndex function builds a page index from a manifest
for faster lookup of page metadata. The title of a page is used for
indexing, since a page title is unique within a Confluence space.
.INPUTS
Manifest
.OUTPUTS
Returns a Hashtable, where the key of each key-value pair is the
page title and attachment name, and the value is the index of the
array item within the Attachments porition of the manifest.
.EXAMPLE
New-PageIndex -Manifest @{}
#>
Param(
[Parameter(Mandatory, ValueFromPipeline)] [Array] $Manifest
)
Process
{
$index = @{}
For($i = 0; $i -lt $Manifest.Count; $i += 1)
{
$index[$Manifest[$i].Title] = $i
}
$index
}
}
function New-AttachmentsManifestIndex
{
<#
.SYNOPSIS
Create an index of page container attachments from a manifest
.DESCRIPTION
The New-AttachmentIndex function builds an attachment index from a
manifest for faster lookup of attachment metadata. The title of
the container page, including the attachment name a is used for
indexing, since attachment names are unique within a container page.
.INPUTS
Manifest
.OUTPUTS
Returns a Hashtable, where the key of each key-value pair is the
interpolation of the container page title and attachment name, and
the value is the index of the array item within the Attachments
porition of the manifest.
.EXAMPLE
New-AttachmentIndex -Manifest @{}
#>
Param(
#manifest
[Parameter(Mandatory, ValueFromPipeline)] [Array] $Manifest
)
Process
{
$index = @{}
For($i = 0; $i -lt $Manifest.Count; $i += 1)
{
$key = "$($Manifest[$i].ContainerPageTitle):" + `
"$($Manifest[$i].Name)"
$index[$key] = $i
}
$index
}
}

View file

@ -6,19 +6,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"pages": { "pages": {
"type": "object", "type": "array",
"patternProperties": { "item": {
".*": { "$ref": "#/definitions/page"
"$ref": "#/definitions/page"
}
} }
}, },
"attachments": { "attachments": {
"type": "object", "type": "array",
"patternProperties": { "item": {
".*": { "$ref": "#/definitions/attachment"
"$ref": "#/definitions/attachment"
}
} }
} }
}, },
@ -31,7 +27,11 @@
"type": "object", "type": "object",
"description": "Local Confluence page/container attachment metadata", "description": "Local Confluence page/container attachment metadata",
"properties": { "properties": {
"PageId": { "Title": {
"type": "string",
"description": "Title of page"
},
"Id": {
"type": "string", "type": "string",
"description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of a page." "description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of a page."
}, },
@ -52,7 +52,7 @@
} }
}, },
"required": [ "required": [
"Hash", "Title",
"Ref" "Ref"
] ]
}, },
@ -60,7 +60,11 @@
"type": "object", "type": "object",
"description": "Local Confluence page/container attachment metadata", "description": "Local Confluence page/container attachment metadata",
"properties": { "properties": {
"AttachmentId": { "Name": {
"type": "string",
"description": "name of attachment, which must be unique within the container page"
},
"Id": {
"type": "string", "type": "string",
"description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of an attachment." "description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of an attachment."
}, },
@ -83,6 +87,7 @@
} }
}, },
"required": [ "required": [
"Name",
"Hash", "Hash",
"MimeType", "MimeType",
"ContainerPageTitle", "ContainerPageTitle",