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 {
static [ BaseNode ] ParseNodeFromObject ( $jsonObj ) {
if ( $jsonObj . NodeType -eq [ HeaderNode ]. Name ) {
return [ HeaderNode ]:: FromJsonObject ( $jsonObj )
2022-12-14 10:24:47 +01:00
} elseif ( $jsonObj . NodeType -eq [ ToolVersionNode ]. Name ) {
return [ ToolVersionNode ]:: FromJsonObject ( $jsonObj )
} elseif ( $jsonObj . NodeType -eq [ ToolVersionsListNode ]. Name ) {
return [ ToolVersionsListNode ]:: FromJsonObject ( $jsonObj )
2022-12-07 14:20:14 +01:00
} elseif ( $jsonObj . NodeType -eq [ TableNode ]. Name ) {
return [ TableNode ]:: FromJsonObject ( $jsonObj )
} elseif ( $jsonObj . NodeType -eq [ NoteNode ]. Name ) {
return [ NoteNode ]:: FromJsonObject ( $jsonObj )
}
throw "Unknown node type in ParseNodeFromObject ' $( $jsonObj . NodeType ) '"
}
}
# Node type to describe headers: "## Installed software"
class HeaderNode: BaseNode {
[ String ] $Title
[ System.Collections.ArrayList ] $Children
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 ) "
}
$this . Children . Add ( $node )
}
[ void ] AddNodes ([ Array ] $nodes ) {
$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-14 10:24:47 +01:00
[ void ] AddToolVersionsList ([ String ] $ToolName , [ Array ] $Version , [ String ] $MajorVersionRegex , [ Boolean ] $InlineList ) {
$this . AddNode ([ ToolVersionsListNode ]:: new ( $ToolName , $Version , $MajorVersionRegex , $InlineList ))
2022-12-07 14:20:14 +01:00
}
2022-12-14 10:24:47 +01:00
[ void ] AddTable ([ Array ] $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 ))
}
[ String ] ToMarkdown ( $level ) {
$sb = [ System.Text.StringBuilder ]:: new ()
$sb . AppendLine ()
$sb . AppendLine ( " $( "#" * $level ) $( $this . Title ) " )
$this . Children | ForEach-Object {
$sb . AppendLine ( $_ . ToMarkdown ( $level + 1 ))
}
return $sb . ToString (). TrimEnd ()
}
[ PSCustomObject ] ToJsonObject () {
return [ PSCustomObject ] @ {
NodeType = $this . GetType (). Name
Title = $this . Title
Children = $this . Children | ForEach-Object { $_ . ToJsonObject () }
}
}
static [ HeaderNode ] FromJsonObject ( $jsonObj ) {
$node = [ HeaderNode ]:: new ( $jsonObj . Title )
$jsonObj . Children | Where-Object { $_ } | ForEach-Object { $node . AddNode ([ NodesFactory ]:: ParseNodeFromObject ( $_ )) }
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
}
}
# Node type to describe the tool with single version: "Bash 5.1.16"
2022-12-14 10:24:47 +01:00
class ToolVersionNode: BaseToolNode {
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
}
[ String ] ToMarkdown ( $level ) {
return "- $( $this . ToolName ) $( $this . Version ) "
}
[ String ] GetValue () {
return $this . Version
}
[ PSCustomObject ] ToJsonObject () {
return [ PSCustomObject ] @ {
NodeType = $this . GetType (). Name
ToolName = $this . ToolName
Version = $this . Version
}
}
static [ BaseNode ] FromJsonObject ( $jsonObj ) {
2022-12-14 10:24:47 +01:00
return [ ToolVersionNode ]:: new ( $jsonObj . ToolName , $jsonObj . Version )
2022-12-07 14:20:14 +01:00
}
}
# Node type to describe the tool with multiple versions "Toolcache Node.js 14.17.6 16.2.0 18.2.3"
2022-12-14 10:24:47 +01:00
class ToolVersionsListNode: BaseToolNode {
2022-12-07 14:20:14 +01:00
[ Array ] $Versions
2022-12-13 16:54:41 +01:00
[ Regex ] $MajorVersionRegex
[ String ] $ListType
2022-12-07 14:20:14 +01:00
2022-12-14 10:24:47 +01:00
ToolVersionsListNode ([ String ] $ToolName , [ Array ] $Versions , [ String ] $MajorVersionRegex , [ Boolean ] $InlineList ) : 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 )
$this . ListType = $InlineList ? "Inline" : "List"
$this . ValidateMajorVersionRegex ()
2022-12-07 14:20:14 +01:00
}
[ String ] ToMarkdown ( $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 ()
$sb . AppendLine ( " $( "#" * $level ) $( $this . ToolName ) " )
$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 )
if ( $match . Success -ne $true ) {
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-14 10:24:47 +01:00
static [ ToolVersionsListNode ] FromJsonObject ( $jsonObj ) {
return [ ToolVersionsListNode ]:: new ( $jsonObj . ToolName , $jsonObj . Versions , $jsonObj . MajorVersionRegex , $jsonObj . ListType -eq "Inline" )
2022-12-13 16:54:41 +01:00
}
hidden [ void ] ValidateMajorVersionRegex () {
$this . Versions | Group-Object { $this . ExtractMajorVersion ( $_ ) } | ForEach-Object {
if ( $_ . Count -gt 1 ) {
throw "Multiple versions from list $( $this . GetValue ()) return the same result from regex ' $( $this . MajorVersionRegex ) ': $( $_ . Name ) "
}
}
2022-12-07 14:20:14 +01:00
}
}
# Node type to describe tables
class TableNode: BaseNode {
# It is easier to store the table as rendered lines because it will simplify finding differences in rows later
[ String ] $Headers
[ System.Collections.ArrayList ] $Rows
TableNode ( $Headers , $Rows ) {
$this . Headers = $Headers
$this . Rows = $Rows
}
[ Boolean ] ShouldBeIncludedToDiff () {
2022-12-13 16:54:41 +01:00
return $true
2022-12-07 14:20:14 +01:00
}
static [ TableNode ] FromObjectsArray ([ Array ] $Table ) {
# take column names from the first row in table because we expect all rows to have the same columns
[ String ] $tableHeaders = [ TableNode ]:: ArrayToTableRow ( $Table [ 0 ]. PSObject . Properties . Name )
[ System.Collections.ArrayList ] $tableRows = @ ()
$Table | ForEach-Object {
$tableRows . Add ([ TableNode ]:: ArrayToTableRow ( $_ . PSObject . Properties . Value ))
}
return [ TableNode ]:: new ( $tableHeaders , $tableRows )
}
[ String ] ToMarkdown ( $level ) {
$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 ])
}
}
$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 ()
}
[ PSCustomObject ] ToJsonObject () {
return [ PSCustomObject ] @ {
NodeType = $this . GetType (). Name
Headers = $this . Headers
Rows = $this . Rows
}
}
static [ TableNode ] FromJsonObject ( $jsonObj ) {
return [ TableNode ]:: new ( $jsonObj . Headers , $jsonObj . Rows )
}
[ 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
}
if ( $this . Headers -ne $OtherNode . Headers ) {
return $false
}
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
}
hidden static [ String ] ArrayToTableRow ([ Array ] $Values ) {
# TO-DO: Add validation for the case when $Values contains "|"
return [ String ]:: Join ( "|" , $Values )
}
}
class NoteNode: BaseNode {
[ String ] $Content
NoteNode ([ String ] $Content ) {
$this . Content = $Content
}
[ String ] ToMarkdown ( $level ) {
return @ (
'```' ,
$this . Content ,
'```'
) -join " `n "
}
[ PSCustomObject ] ToJsonObject () {
return [ PSCustomObject ] @ {
NodeType = $this . GetType (). Name
Content = $this . Content
}
}
static [ NoteNode ] FromJsonObject ( $jsonObj ) {
return [ NoteNode ]:: new ( $jsonObj . Content )
}
[ 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 )
}
}