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-21 10:58:27 +01:00
[ Array ] $existingHeaderNodes = $this . Children | Where-Object { $_ -is [ HeaderNode ] }
if (( $existingHeaderNodes . Count -gt 0 ) -and ( $node -isnot [ HeaderNode ])) {
throw "It is not allowed to add the node of type ' $( $node . GetType (). Name ) ' to the HeaderNode that already contains the HeaderNode children."
}
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 ) {
2022-12-07 14:20:14 +01:00
$this . AddNode ([ TableNode ]:: FromObjectsArray ( $Table ))
}
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-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 ) {
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
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
$delimeterLine = [ String ]:: Join ( "|" , @ ( "-" ) * $columnsCount )
$sb = [ System.Text.StringBuilder ]:: new ()
@ ( $this . Headers ) + @ ( $delimeterLine ) + $this . Rows | ForEach-Object {
$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 )
}
}