2022-12-07 14:20:14 +01:00
using module . / SoftwareReport . BaseNodes . psm1
#########################################
### Nodes to describe image software ####
#########################################
# NodesFactory is used to simplify parsing different types of notes
# Every node has own logic of parsing and this method just invokes "FromJsonObject" of correct node type
class NodesFactory {
2022-12-21 10:58:27 +01:00
static [ BaseNode ] ParseNodeFromObject ( [ object ] $JsonObj ) {
if ( $JsonObj . NodeType -eq [ HeaderNode ] . Name ) {
return [ HeaderNode ] :: FromJsonObject ( $JsonObj )
} elseif ( $JsonObj . NodeType -eq [ ToolVersionNode ] . Name ) {
return [ ToolVersionNode ] :: FromJsonObject ( $JsonObj )
} elseif ( $JsonObj . NodeType -eq [ ToolVersionsListNode ] . Name ) {
return [ ToolVersionsListNode ] :: FromJsonObject ( $JsonObj )
} elseif ( $JsonObj . NodeType -eq [ TableNode ] . Name ) {
return [ TableNode ] :: FromJsonObject ( $JsonObj )
} elseif ( $JsonObj . NodeType -eq [ NoteNode ] . Name ) {
return [ NoteNode ] :: FromJsonObject ( $JsonObj )
2022-12-07 14:20:14 +01:00
}
2022-12-21 10:58:27 +01:00
throw " Unknown node type in ParseNodeFromObject ' $( $JsonObj . NodeType ) ' "
2022-12-07 14:20:14 +01:00
}
}
class HeaderNode: BaseNode {
2022-12-21 10:58:27 +01:00
[ ValidateNotNullOrEmpty ( ) ]
2022-12-07 14:20:14 +01:00
[ String ] $Title
2022-12-21 10:58:27 +01:00
[ Collections.Generic.List[BaseNode] ] $Children
2022-12-07 14:20:14 +01:00
HeaderNode ( [ String ] $Title ) {
$this . Title = $Title
$this . Children = @ ( )
}
[ Boolean ] ShouldBeIncludedToDiff ( ) {
2022-12-13 16:54:41 +01:00
return $true
2022-12-07 14:20:14 +01:00
}
[ void ] AddNode ( [ BaseNode ] $node ) {
$similarNode = $this . FindSimilarChildNode ( $node )
if ( $similarNode ) {
throw " This HeaderNode already contains the similar child node. It is not allowed to add the same node twice. `n Found node: $( $similarNode . ToJsonObject ( ) | ConvertTo-Json ) `n New node: $( $node . ToJsonObject ( ) | ConvertTo-Json ) "
}
2022-12-23 21:02:07 +01:00
if ( -not $this . IsNodeHasMarkdownHeader ( $node ) ) {
# If the node doesn't print own header to markdown, we should check that there is no other nodes that print header to markdown before it.
# It is done to avoid unexpected situation like this:
#
# HeaderNode A -> # A
# HeaderNode B -> ## B
# ToolVersionNode C -> - C
# ToolVersionNode D -> - D
#
# In this example, we add 'HeaderNode B" to 'HeaderNode A' and add 'ToolVersionNode C' to 'HeaderNode B'.
# Then we add 'ToolVersionNode D' to 'HeaderNode A'.
# But the result markdown will look like 'ToolVersionNode D' belongs to 'HeaderNode B' instead of 'HeaderNode A'.
$this . Children | Where-Object { $this . IsNodeHasMarkdownHeader ( $_ ) } | ForEach-Object {
throw " It is not allowed to add the non-header node after the header node. Consider adding the separate HeaderNode for this node "
}
2022-12-21 10:58:27 +01:00
}
2022-12-07 14:20:14 +01:00
$this . Children . Add ( $node )
}
2022-12-21 10:58:27 +01:00
[ void ] AddNodes ( [ BaseNode[] ] $nodes ) {
2022-12-07 14:20:14 +01:00
$nodes | ForEach-Object {
$this . AddNode ( $_ )
}
}
2022-12-14 10:24:47 +01:00
[ HeaderNode ] AddHeader ( [ String ] $Title ) {
2022-12-07 14:20:14 +01:00
$node = [ HeaderNode ] :: new ( $Title )
$this . AddNode ( $node )
return $node
}
2022-12-14 10:24:47 +01:00
[ void ] AddToolVersion ( [ String ] $ToolName , [ String ] $Version ) {
$this . AddNode ( [ ToolVersionNode ] :: new ( $ToolName , $Version ) )
2022-12-07 14:20:14 +01:00
}
2022-12-21 10:58:27 +01:00
[ void ] AddToolVersionsList ( [ String ] $ToolName , [ String[] ] $Version , [ String ] $MajorVersionRegex ) {
$this . AddNode ( [ ToolVersionsListNode ] :: new ( $ToolName , $Version , $MajorVersionRegex , " List " ) )
}
[ void ] AddToolVersionsListInline ( [ String ] $ToolName , [ String[] ] $Version , [ String ] $MajorVersionRegex ) {
$this . AddNode ( [ ToolVersionsListNode ] :: new ( $ToolName , $Version , $MajorVersionRegex , " Inline " ) )
2022-12-07 14:20:14 +01:00
}
2022-12-21 10:58:27 +01:00
[ void ] AddTable ( [ PSCustomObject[] ] $Table ) {
2024-09-05 12:19:58 +02:00
$this . AddNode ( [ TableNode ] :: FromObjectsArray ( $Table ) )
2022-12-07 14:20:14 +01:00
}
2022-12-14 10:24:47 +01:00
[ void ] AddNote ( [ String ] $Content ) {
2022-12-07 14:20:14 +01:00
$this . AddNode ( [ NoteNode ] :: new ( $Content ) )
}
2022-12-21 10:58:27 +01:00
[ String ] ToMarkdown ( [ Int32 ] $Level ) {
2022-12-07 14:20:14 +01:00
$sb = [ System.Text.StringBuilder ] :: new ( )
$sb . AppendLine ( )
2022-12-21 10:58:27 +01:00
$sb . AppendLine ( " $( " # " * $Level ) $( $this . Title ) " )
2022-12-07 14:20:14 +01:00
$this . Children | ForEach-Object {
2022-12-21 10:58:27 +01:00
$sb . AppendLine ( $_ . ToMarkdown ( $Level + 1 ) )
2022-12-07 14:20:14 +01:00
}
return $sb . ToString ( ) . TrimEnd ( )
}
[ PSCustomObject ] ToJsonObject ( ) {
return [ PSCustomObject ] @ {
NodeType = $this . GetType ( ) . Name
Title = $this . Title
Children = $this . Children | ForEach-Object { $_ . ToJsonObject ( ) }
}
}
2022-12-21 10:58:27 +01:00
static [ HeaderNode ] FromJsonObject ( [ Object ] $JsonObj ) {
$node = [ HeaderNode ] :: new ( $JsonObj . Title )
$JsonObj . Children | Where-Object { $_ } | ForEach-Object { $node . AddNode ( [ NodesFactory ] :: ParseNodeFromObject ( $_ ) ) }
2022-12-07 14:20:14 +01:00
return $node
}
[ Boolean ] IsSimilarTo ( [ BaseNode ] $OtherNode ) {
if ( $OtherNode . GetType ( ) -ne [ HeaderNode ] ) {
return $false
}
return $this . Title -eq $OtherNode . Title
}
[ Boolean ] IsIdenticalTo ( [ BaseNode ] $OtherNode ) {
return $this . IsSimilarTo ( $OtherNode )
}
[ BaseNode ] FindSimilarChildNode ( [ BaseNode ] $Find ) {
foreach ( $childNode in $this . Children ) {
if ( $childNode . IsSimilarTo ( $Find ) ) {
return $childNode
}
}
return $null
}
2022-12-23 21:02:07 +01:00
hidden [ Boolean ] IsNodeHasMarkdownHeader ( [ BaseNode ] $node ) {
if ( $node -is [ HeaderNode ] ) {
return $true
}
if ( ( $node -is [ ToolVersionsListNode ] ) -and ( $node . ListType -eq " List " ) ) {
return $true
}
return $false
}
2022-12-07 14:20:14 +01:00
}
2022-12-14 10:24:47 +01:00
class ToolVersionNode: BaseToolNode {
2022-12-21 10:58:27 +01:00
[ ValidateNotNullOrEmpty ( ) ]
2022-12-07 14:20:14 +01:00
[ String ] $Version
2022-12-14 10:24:47 +01:00
ToolVersionNode ( [ String ] $ToolName , [ String ] $Version ) : base ( $ToolName ) {
2023-08-24 18:40:20 +02:00
if ( [ String ] :: IsNullOrEmpty ( $Version ) ) {
throw " ToolVersionNode ' $( $this . ToolName ) ' has empty version "
}
2022-12-07 14:20:14 +01:00
$this . Version = $Version
}
2022-12-21 10:58:27 +01:00
[ String ] ToMarkdown ( [ Int32 ] $Level ) {
2022-12-07 14:20:14 +01:00
return " - $( $this . ToolName ) $( $this . Version ) "
}
[ String ] GetValue ( ) {
return $this . Version
}
[ PSCustomObject ] ToJsonObject ( ) {
return [ PSCustomObject ] @ {
NodeType = $this . GetType ( ) . Name
ToolName = $this . ToolName
Version = $this . Version
}
}
2022-12-21 10:58:27 +01:00
static [ BaseNode ] FromJsonObject ( [ Object ] $JsonObj ) {
return [ ToolVersionNode ] :: new ( $JsonObj . ToolName , $JsonObj . Version )
2022-12-07 14:20:14 +01:00
}
}
2022-12-14 10:24:47 +01:00
class ToolVersionsListNode: BaseToolNode {
2022-12-21 10:58:27 +01:00
[ ValidateNotNullOrEmpty ( ) ]
[ String[] ] $Versions
2022-12-13 16:54:41 +01:00
[ Regex ] $MajorVersionRegex
2022-12-21 10:58:27 +01:00
[ ValidateSet ( " List " , " Inline " ) ]
2022-12-13 16:54:41 +01:00
[ String ] $ListType
2022-12-07 14:20:14 +01:00
2022-12-21 10:58:27 +01:00
ToolVersionsListNode ( [ String ] $ToolName , [ String[] ] $Versions , [ String ] $MajorVersionRegex , [ String ] $ListType ) : base ( $ToolName ) {
2022-12-07 14:20:14 +01:00
$this . Versions = $Versions
2023-08-24 18:40:20 +02:00
if ( [ String ] :: IsNullOrEmpty ( $Versions ) ) {
throw " ToolVersionsListNode ' $( $this . ToolName ) ' has empty versions list "
}
2022-12-13 16:54:41 +01:00
$this . MajorVersionRegex = [ Regex ] :: new ( $MajorVersionRegex )
2022-12-21 10:58:27 +01:00
$this . ListType = $ListType
2022-12-13 16:54:41 +01:00
$this . ValidateMajorVersionRegex ( )
2022-12-07 14:20:14 +01:00
}
2022-12-21 10:58:27 +01:00
[ String ] ToMarkdown ( [ Int32 ] $Level ) {
2022-12-13 16:54:41 +01:00
if ( $this . ListType -eq " Inline " ) {
return " - $( $this . ToolName ) : $( $this . Versions -join ', ' ) "
}
2022-12-07 14:20:14 +01:00
$sb = [ System.Text.StringBuilder ] :: new ( )
$sb . AppendLine ( )
2022-12-21 10:58:27 +01:00
$sb . AppendLine ( " $( " # " * $Level ) $( $this . ToolName ) " )
2022-12-07 14:20:14 +01:00
$this . Versions | ForEach-Object {
$sb . AppendLine ( " - $_ " )
}
return $sb . ToString ( ) . TrimEnd ( )
}
[ String ] GetValue ( ) {
return $this . Versions -join ', '
}
2022-12-13 16:54:41 +01:00
[ String ] ExtractMajorVersion ( [ String ] $Version ) {
$match = $this . MajorVersionRegex . Match ( $Version )
2022-12-21 10:58:27 +01:00
if ( ( $match . Success -ne $true ) -or [ String ] :: IsNullOrEmpty ( $match . Groups [ 0 ] . Value ) ) {
2022-12-13 16:54:41 +01:00
throw " Version ' $Version ' doesn't match regex ' $( $this . PrimaryVersionRegex ) ' "
}
return $match . Groups [ 0 ] . Value
}
2022-12-07 14:20:14 +01:00
[ PSCustomObject ] ToJsonObject ( ) {
return [ PSCustomObject ] @ {
NodeType = $this . GetType ( ) . Name
ToolName = $this . ToolName
Versions = $this . Versions
2022-12-13 16:54:41 +01:00
MajorVersionRegex = $this . MajorVersionRegex . ToString ( )
ListType = $this . ListType
2022-12-07 14:20:14 +01:00
}
}
2022-12-21 10:58:27 +01:00
static [ ToolVersionsListNode ] FromJsonObject ( [ Object ] $JsonObj ) {
return [ ToolVersionsListNode ] :: new ( $JsonObj . ToolName , $JsonObj . Versions , $JsonObj . MajorVersionRegex , $JsonObj . ListType )
2022-12-13 16:54:41 +01:00
}
hidden [ void ] ValidateMajorVersionRegex ( ) {
$this . Versions | Group-Object { $this . ExtractMajorVersion ( $_ ) } | ForEach-Object {
if ( $_ . Count -gt 1 ) {
2022-12-21 10:58:27 +01:00
throw " Multiple versions from list ' $( $this . GetValue ( ) ) ' return the same result from regex ' $( $this . MajorVersionRegex ) ': $( $_ . Name ) "
2022-12-13 16:54:41 +01:00
}
}
2022-12-07 14:20:14 +01:00
}
}
class TableNode: BaseNode {
# It is easier to store the table as rendered lines because it will simplify finding differences in rows later
2022-12-21 10:58:27 +01:00
[ ValidateNotNullOrEmpty ( ) ]
2022-12-07 14:20:14 +01:00
[ String ] $Headers
2022-12-21 10:58:27 +01:00
[ ValidateNotNullOrEmpty ( ) ]
[ String[] ] $Rows
2022-12-07 14:20:14 +01:00
2022-12-21 10:58:27 +01:00
TableNode ( [ String ] $Headers , [ String[] ] $Rows ) {
2022-12-07 14:20:14 +01:00
$this . Headers = $Headers
$this . Rows = $Rows
2022-12-21 10:58:27 +01:00
$columnsCount = $this . Headers . Split ( " | " ) . Count
$this . Rows | ForEach-Object {
if ( $_ . Split ( " | " ) . Count -ne $columnsCount ) {
throw " Table has different number of columns in different rows "
}
}
2022-12-07 14:20:14 +01:00
}
[ Boolean ] ShouldBeIncludedToDiff ( ) {
2022-12-13 16:54:41 +01:00
return $true
2022-12-07 14:20:14 +01:00
}
2022-12-21 10:58:27 +01:00
[ String ] ToMarkdown ( [ Int32 ] $Level ) {
$maxColumnWidths = $this . CalculateColumnsWidth ( )
2022-12-07 14:20:14 +01:00
$columnsCount = $maxColumnWidths . Count
2023-05-24 17:14:46 +09:00
$delimiterLine = [ String ] :: Join ( " | " , @ ( " - " ) * $columnsCount )
2022-12-07 14:20:14 +01:00
$sb = [ System.Text.StringBuilder ] :: new ( )
2023-05-24 17:14:46 +09:00
@ ( $this . Headers ) + @ ( $delimiterLine ) + $this . Rows | ForEach-Object {
2022-12-07 14:20:14 +01:00
$sb . Append ( " | " )
$row = $_ . Split ( " | " )
for ( $colIndex = 0 ; $colIndex -lt $columnsCount ; $colIndex + + ) {
$padSymbol = $row [ $colIndex ] -eq " - " ? " - " : " "
$cellContent = $row [ $colIndex ] . PadRight ( $maxColumnWidths [ $colIndex ] , $padSymbol )
$sb . Append ( " $( $cellContent ) | " )
}
$sb . AppendLine ( )
}
return $sb . ToString ( ) . TrimEnd ( )
}
2022-12-21 10:58:27 +01:00
hidden [ Int32[] ] CalculateColumnsWidth ( ) {
$maxColumnWidths = $this . Headers . Split ( " | " ) | ForEach-Object { $_ . Length }
$columnsCount = $maxColumnWidths . Count
$this . Rows | ForEach-Object {
$columnWidths = $_ . Split ( " | " ) | ForEach-Object { $_ . Length }
for ( $colIndex = 0 ; $colIndex -lt $columnsCount ; $colIndex + + ) {
$maxColumnWidths [ $colIndex ] = [ Math ] :: Max ( $maxColumnWidths [ $colIndex ] , $columnWidths [ $colIndex ] )
}
}
return $maxColumnWidths
}
2022-12-07 14:20:14 +01:00
[ PSCustomObject ] ToJsonObject ( ) {
return [ PSCustomObject ] @ {
NodeType = $this . GetType ( ) . Name
Headers = $this . Headers
Rows = $this . Rows
}
}
2022-12-21 10:58:27 +01:00
static [ TableNode ] FromJsonObject ( [ Object ] $JsonObj ) {
return [ TableNode ] :: new ( $JsonObj . Headers , $JsonObj . Rows )
2022-12-07 14:20:14 +01:00
}
[ Boolean ] IsSimilarTo ( [ BaseNode ] $OtherNode ) {
if ( $OtherNode . GetType ( ) -ne [ TableNode ] ) {
return $false
}
# We don't support having multiple TableNode instances on the same header level so such check is fine
return $true
}
[ Boolean ] IsIdenticalTo ( [ BaseNode ] $OtherNode ) {
if ( -not $this . IsSimilarTo ( $OtherNode ) ) {
return $false
}
2022-12-21 10:58:27 +01:00
# We don't compare $this.Headers intentionally
# It is fine to ignore the tables where headers are changed but rows are not changed
2022-12-07 14:20:14 +01:00
if ( $this . Rows . Count -ne $OtherNode . Rows . Count ) {
return $false
}
for ( $rowIndex = 0 ; $rowIndex -lt $this . Rows . Count ; $rowIndex + + ) {
if ( $this . Rows [ $rowIndex ] -ne $OtherNode . Rows [ $rowIndex ] ) {
return $false
}
}
return $true
}
2022-12-21 10:58:27 +01:00
static [ TableNode ] FromObjectsArray ( [ PSCustomObject[] ] $Table ) {
if ( $Table . Count -eq 0 ) {
throw " Failed to create TableNode from empty objects array "
}
[ String ] $tableHeaders = [ TableNode ] :: ArrayToTableRow ( $Table [ 0 ] . PSObject . Properties . Name )
[ Collections.Generic.List[String] ] $tableRows = @ ( )
$Table | ForEach-Object {
$rowHeaders = [ TableNode ] :: ArrayToTableRow ( $_ . PSObject . Properties . Name )
if ( ( $rowHeaders -ne $tableHeaders ) ) {
throw " Failed to create TableNode from objects array because objects have different properties "
}
$tableRows . Add ( [ TableNode ] :: ArrayToTableRow ( $_ . PSObject . Properties . Value ) )
}
return [ TableNode ] :: new ( $tableHeaders , $tableRows )
}
hidden static [ String ] ArrayToTableRow ( [ String[] ] $Values ) {
if ( $Values . Count -eq 0 ) {
throw " Failed to create TableNode because some objects are empty "
}
$Values | ForEach-Object {
if ( $_ . Contains ( " | " ) ) {
throw " Failed to create TableNode because some cells ' $_ ' contains forbidden symbol '|' "
}
}
2022-12-07 14:20:14 +01:00
return [ String ] :: Join ( " | " , $Values )
}
}
class NoteNode: BaseNode {
2022-12-21 10:58:27 +01:00
[ ValidateNotNullOrEmpty ( ) ]
2022-12-07 14:20:14 +01:00
[ String ] $Content
NoteNode ( [ String ] $Content ) {
$this . Content = $Content
}
2022-12-21 10:58:27 +01:00
[ String ] ToMarkdown ( [ Int32 ] $Level ) {
2022-12-07 14:20:14 +01:00
return @ (
'```' ,
$this . Content ,
'```'
) -join " `n "
}
[ PSCustomObject ] ToJsonObject ( ) {
return [ PSCustomObject ] @ {
NodeType = $this . GetType ( ) . Name
Content = $this . Content
}
}
2022-12-21 10:58:27 +01:00
static [ NoteNode ] FromJsonObject ( [ Object ] $JsonObj ) {
return [ NoteNode ] :: new ( $JsonObj . Content )
2022-12-07 14:20:14 +01:00
}
[ Boolean ] IsSimilarTo ( [ BaseNode ] $OtherNode ) {
if ( $OtherNode . GetType ( ) -ne [ NoteNode ] ) {
return $false
}
return $this . Content -eq $OtherNode . Content
}
[ Boolean ] IsIdenticalTo ( [ BaseNode ] $OtherNode ) {
return $this . IsSimilarTo ( $OtherNode )
}
}