diff --git a/PSConfluencePublisher/Manifest.Tests.ps1 b/PSConfluencePublisher/Manifest.Tests.ps1 index afbdecb..93884c8 100755 --- a/PSConfluencePublisher/Manifest.Tests.ps1 +++ b/PSConfluencePublisher/Manifest.Tests.ps1 @@ -5,6 +5,7 @@ BeforeAll { Import-Module (Join-Path $PSScriptRoot 'PSConfluencePublisher.psd1') -Force } + AfterAll { } @@ -19,7 +20,7 @@ Describe 'Get-Manifest' ` InModuleScope Manifest ` { Mock Get-Content { - return '{"pages":{}, "attachments": {}}' + return '{"pages":[], "attachments": []}' } #mocking Get-Content, therefore file name can be bogus @@ -32,7 +33,7 @@ Describe 'Get-Manifest' ` InModuleScope Manifest ` { Mock Get-Content { - return '{"pagges":{}, "attsdachments": {}}' + return '{"pagges":[], "attsdachments": []}' } #mocking Get-Content, therefore file name can be bogus @@ -52,8 +53,8 @@ Describe 'Set-Manifest' ` InModuleScope Manifest ` { $mockManifest = @{ - 'pages' = @{} - 'attachments' = @{} + 'pages' = @() + 'attachments' = @() } Mock Set-Content { @@ -76,8 +77,8 @@ Describe 'Set-Manifest' ` InModuleScope Manifest ` { $mockManifest = @{ - 'pagges' = @{} - 'attachments' = @{} + 'pagges' = @() + 'attachments' = @() } #mocking Get-Content, therefore file name can be bogus @@ -97,8 +98,8 @@ Describe 'Set-Manifest' ` InModuleScope Manifest ` { $mockManifest = @{ - 'pages' = @{} - 'attachments' = @{} + 'pages' = @() + 'attachments' = @() } Mock Set-Content { @@ -128,8 +129,8 @@ Describe 'Set-Manifest' ` InModuleScope Manifest ` { $mockManifest = @{ - 'pages' = @{} - 'attachments' = @{} + 'pages' = @() + 'attachments' = @() } 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' + } + } +} diff --git a/PSConfluencePublisher/Manifest.psm1 b/PSConfluencePublisher/Manifest.psm1 index 7fa0a22..d285b51 100755 --- a/PSConfluencePublisher/Manifest.psm1 +++ b/PSConfluencePublisher/Manifest.psm1 @@ -89,3 +89,270 @@ function Set-Manifest 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 + } +} + diff --git a/PSConfluencePublisher/manifest.schema.json b/PSConfluencePublisher/manifest.schema.json index 1910e70..71d683b 100755 --- a/PSConfluencePublisher/manifest.schema.json +++ b/PSConfluencePublisher/manifest.schema.json @@ -6,19 +6,15 @@ "type": "object", "properties": { "pages": { - "type": "object", - "patternProperties": { - ".*": { - "$ref": "#/definitions/page" - } + "type": "array", + "item": { + "$ref": "#/definitions/page" } }, "attachments": { - "type": "object", - "patternProperties": { - ".*": { - "$ref": "#/definitions/attachment" - } + "type": "array", + "item": { + "$ref": "#/definitions/attachment" } } }, @@ -31,7 +27,11 @@ "type": "object", "description": "Local Confluence page/container attachment metadata", "properties": { - "PageId": { + "Title": { + "type": "string", + "description": "Title of page" + }, + "Id": { "type": "string", "description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of a page." }, @@ -52,7 +52,7 @@ } }, "required": [ - "Hash", + "Title", "Ref" ] }, @@ -60,7 +60,11 @@ "type": "object", "description": "Local Confluence page/container attachment metadata", "properties": { - "AttachmentId": { + "Name": { + "type": "string", + "description": "name of attachment, which must be unique within the container page" + }, + "Id": { "type": "string", "description": "Id of attachment defined by Confluence instance. The id is generated after the publishing of an attachment." }, @@ -83,6 +87,7 @@ } }, "required": [ + "Name", "Hash", "MimeType", "ContainerPageTitle",