DscResource.Authoring.psm1
|
#Region './prefix.ps1' -1 $script:AdaptedResourceSchemaUri = 'https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json' $script:ResourceManifestSchemaUri = 'https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json' $script:JsonSchemaUri = 'https://json-schema.org/draft/2020-12/schema' $script:DefaultAdapter = 'Microsoft.Adapter/PowerShell' #EndRegion './prefix.ps1' 5 #Region './Classes/001.DscAdaptedResourceManifestSchema.ps1' -1 class DscAdaptedResourceManifestSchema { [hashtable] $Embedded } #EndRegion './Classes/001.DscAdaptedResourceManifestSchema.ps1' 5 #Region './Classes/002.DscAdaptedResourceManifest.ps1' -1 class DscAdaptedResourceManifest { [string] $Schema [string] $Type [string] $Kind [string] $Version [string[]] $Capabilities [string] $Description [string] $Author [string] $RequireAdapter [string] $Path [DscAdaptedResourceManifestSchema] $ManifestSchema [string] ToJson() { $manifest = [ordered]@{ '$schema' = $this.Schema type = $this.Type kind = $this.Kind version = $this.Version capabilities = $this.Capabilities description = $this.Description author = $this.Author requireAdapter = $this.RequireAdapter path = $this.Path schema = [ordered]@{ embedded = $this.ManifestSchema.Embedded } } return $manifest | ConvertTo-Json -Depth 10 } [hashtable] ToHashtable() { return [ordered]@{ '$schema' = $this.Schema type = $this.Type kind = $this.Kind version = $this.Version capabilities = $this.Capabilities description = $this.Description author = $this.Author requireAdapter = $this.RequireAdapter path = $this.Path schema = [ordered]@{ embedded = $this.ManifestSchema.Embedded } } } } #EndRegion './Classes/002.DscAdaptedResourceManifest.ps1' 51 #Region './Classes/003.DscPropertyOverride.ps1' -1 class DscPropertyOverride { [string] $Name [string] $Description [string] $Title [hashtable] $JsonSchema [string[]] $RemoveKeys [object] $Required DscPropertyOverride() { $this.JsonSchema = @{} $this.RemoveKeys = @() } } #EndRegion './Classes/003.DscPropertyOverride.ps1' 16 #Region './Classes/004.DscResourceManifestList.ps1' -1 class DscResourceManifestList { [System.Collections.Generic.List[hashtable]] $AdaptedResources [System.Collections.Generic.List[hashtable]] $Resources [System.Collections.Generic.List[hashtable]] $Extensions DscResourceManifestList() { $this.AdaptedResources = [System.Collections.Generic.List[hashtable]]::new() $this.Resources = [System.Collections.Generic.List[hashtable]]::new() $this.Extensions = [System.Collections.Generic.List[hashtable]]::new() } [void] AddAdaptedResource([DscAdaptedResourceManifest]$Manifest) { $this.AdaptedResources.Add($Manifest.ToHashtable()) } [void] AddResource([hashtable]$Resource) { $this.Resources.Add($Resource) } [void] AddExtension([hashtable]$Extension) { $this.Extensions.Add($Extension) } [string] ToJson() { $result = [ordered]@{} if ($this.AdaptedResources.Count -gt 0) { $result['adaptedResources'] = @($this.AdaptedResources) } if ($this.Resources.Count -gt 0) { $result['resources'] = @($this.Resources) } if ($this.Extensions.Count -gt 0) { $result['extensions'] = @($this.Extensions) } return $result | ConvertTo-Json -Depth 15 } } #EndRegion './Classes/004.DscResourceManifestList.ps1' 51 #Region './Private/Add-AstProperty.ps1' -1 <# .SYNOPSIS Recursively collects DSC properties from a class type definition AST. .DESCRIPTION Walks the base type chain of the supplied type definition AST and adds a hashtable describing each property decorated with the [DscProperty()] attribute to the supplied list. Properties from base classes are added first so that derived class properties override them when the list is consumed. .PARAMETER AllTypeDefinitions All type definition AST nodes discovered in the script. Used to resolve base class types and enum types defined in the same file. .PARAMETER TypeAst The type definition AST to collect properties from. .PARAMETER Properties The list to which property hashtables are added. Each hashtable contains the property Name, TypeName, IsKey, IsMandatory, IsNotConfigurable and EnumValues. .EXAMPLE $properties = [System.Collections.Generic.List[hashtable]]::new() Add-AstProperty -AllTypeDefinitions $allTypes -TypeAst $typeAst -Properties $properties Collects all [DscProperty()] decorated properties from $typeAst into $properties. #> function Add-AstProperty { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.TypeDefinitionAst[]] $AllTypeDefinitions, [Parameter(Mandatory = $true)] [System.Management.Automation.Language.TypeDefinitionAst] $TypeAst, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[hashtable]] $Properties ) foreach ($typeConstraint in $TypeAst.BaseTypes) { $baseType = $AllTypeDefinitions | Where-Object { $_.Name -eq $typeConstraint.TypeName.Name } if ($baseType) { Add-AstProperty -AllTypeDefinitions $AllTypeDefinitions -TypeAst $baseType -Properties $Properties } } foreach ($member in $TypeAst.Members) { $propertyAst = $member -as [System.Management.Automation.Language.PropertyMemberAst] if (($null -eq $propertyAst) -or ($propertyAst.IsStatic)) { continue } $isDscProperty = $false $isKey = $false $isMandatory = $false $isNotConfigurable = $false $validateSetValues = $null $validatePatternValue = $null foreach ($attr in $propertyAst.Attributes) { if ($attr.TypeName.Name -eq 'DscProperty') { $isDscProperty = $true foreach ($namedArg in $attr.NamedArguments) { switch ($namedArg.ArgumentName) { 'Key' { $isKey = $true } 'Mandatory' { $isMandatory = $true } 'NotConfigurable' { $isNotConfigurable = $true } } } } if ($attr.TypeName.Name -eq 'ValidateSet') { $validateSetValues = @($attr.PositionalArguments | ForEach-Object { $_.Value }) } if ($attr.TypeName.Name -eq 'ValidatePattern') { $validatePatternValue = $attr.PositionalArguments[0].Value } } if (-not $isDscProperty) { continue } $typeName = if ($propertyAst.PropertyType) { $propertyAst.PropertyType.TypeName.Name } else { 'string' } # check if the type is an enum defined in the same file $enumValues = $null $enumAst = $AllTypeDefinitions | Where-Object { $_.Name -eq $typeName -and $_.IsEnum } if ($enumAst) { $enumValues = @($enumAst.Members | ForEach-Object { $_.Name }) } elseif ($validateSetValues) { $enumValues = $validateSetValues } $Properties.Add(@{ Name = $propertyAst.Name TypeName = $typeName IsKey = $isKey IsMandatory = $isMandatory -or $isKey IsNotConfigurable = $isNotConfigurable EnumValues = $enumValues PatternValue = $validatePatternValue }) } } #EndRegion './Private/Add-AstProperty.ps1' 137 #Region './Private/ConvertFrom-CommentBasedHelp.ps1' -1 <# .SYNOPSIS Parses a comment-based help block into a structured hashtable. .DESCRIPTION Extracts the .SYNOPSIS, .DESCRIPTION and .PARAMETER content from a PowerShell block comment and returns the values as a hashtable with Synopsis, Description and Parameters keys. .PARAMETER CommentText The raw text of a PowerShell block comment, including the surrounding known SYNOPSIS delimiters. .EXAMPLE ConvertFrom-CommentBasedHelp -CommentText $token.Text Parses the block comment token text and returns a hashtable with Synopsis, Description and Parameters keys. #> function ConvertFrom-CommentBasedHelp { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $CommentText ) # Strip the <# and #> delimiters $text = $CommentText -replace '^\s*<#', '' -replace '#>\s*$', '' $result = @{ Synopsis = '' Description = '' Parameters = @{} } $keywordPattern = '(?mi)^\s*\.(?<keyword>SYNOPSIS|DESCRIPTION|PARAMETER|EXAMPLE|NOTES|OUTPUTS|INPUTS|LINK|COMPONENT|ROLE|FUNCTIONALITY)[^\S\r\n]*(?<arg>.*)$' $keywordMatches = [regex]::Matches($text, $keywordPattern) if ($keywordMatches.Count -eq 0) { return $result } for ($i = 0; $i -lt $keywordMatches.Count; $i++) { $keyword = $keywordMatches[$i].Groups['keyword'].Value.ToUpper() $arg = $keywordMatches[$i].Groups['arg'].Value.Trim() $startIndex = $keywordMatches[$i].Index + $keywordMatches[$i].Length $endIndex = if ($i + 1 -lt $keywordMatches.Count) { $keywordMatches[$i + 1].Index } else { $text.Length } $rawContent = $text.Substring($startIndex, $endIndex - $startIndex).Trim() # Normalise multi-line content: trim each line and join with a single space # so that descriptions do not contain literal \r\n or leading indentation. $content = ($rawContent -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join ' ' switch ($keyword) { 'SYNOPSIS' { $result.Synopsis = $content } 'DESCRIPTION' { $result.Description = $content } 'PARAMETER' { if (-not [string]::IsNullOrWhiteSpace($arg)) { $result.Parameters[$arg] = $content } } } } return $result } #EndRegion './Private/ConvertFrom-CommentBasedHelp.ps1' 84 #Region './Private/ConvertTo-AdaptedResourceManifest.ps1' -1 <# .SYNOPSIS Hydrates a hashtable into a DscAdaptedResourceManifest object. .DESCRIPTION Maps the keys of an adapted resource manifest hashtable (such as the output of ConvertFrom-Json followed by ConvertTo-Hashtable) onto the properties of a new DscAdaptedResourceManifest instance, including the nested embedded JSON schema. .PARAMETER Hashtable The hashtable representation of an adapted resource manifest document. .EXAMPLE $manifest = ConvertTo-AdaptedResourceManifest -Hashtable $hashtable Hydrates a hashtable parsed from a .dsc.adaptedResource.json file into a DscAdaptedResourceManifest object. #> function ConvertTo-AdaptedResourceManifest { [CmdletBinding()] [OutputType([DscAdaptedResourceManifest])] param ( [Parameter(Mandatory = $true)] [hashtable] $Hashtable ) $manifest = [DscAdaptedResourceManifest]::new() $manifest.Schema = $Hashtable['$schema'] $manifest.Type = $Hashtable['type'] $manifest.Kind = if ($Hashtable.Contains('kind')) { $Hashtable['kind'] } else { 'resource' } $manifest.Version = $Hashtable['version'] $manifest.Capabilities = if ($Hashtable.Contains('capabilities') -and $null -ne $Hashtable['capabilities']) { @($Hashtable['capabilities']) } else { [string[]]::new(0) } $manifest.Description = if ($Hashtable.Contains('description')) { [string]$Hashtable['description'] } else { '' } $manifest.Author = if ($Hashtable.Contains('author')) { [string]$Hashtable['author'] } else { '' } $manifest.RequireAdapter = $Hashtable['requireAdapter'] $manifest.Path = if ($Hashtable.Contains('path')) { [string]$Hashtable['path'] } else { '' } $schemaData = $Hashtable['schema'] if ($schemaData) { $embeddedSchema = if ($schemaData.Contains('embedded')) { $schemaData['embedded'] } else { $schemaData } $manifest.ManifestSchema = [DscAdaptedResourceManifestSchema]@{ Embedded = $embeddedSchema } } return $manifest } #EndRegion './Private/ConvertTo-AdaptedResourceManifest.ps1' 53 #Region './Private/ConvertTo-DscPropertyOverrideFromConfig.ps1' -1 <# .SYNOPSIS Converts property override configuration entries into DscPropertyOverride objects. .DESCRIPTION Maps each hashtable entry from the build configuration PropertyOverrides section into a DscPropertyOverride object understood by Update-DscAdaptedResourceManifest. Each entry must contain at least a 'Name' key. Supported optional keys are 'Description', 'Title', 'JsonSchema', 'RemoveKeys', and 'Required'. This function must only be called after DscResource.Authoring has been imported into the session. .PARAMETER OverrideConfig An array of hashtables, each describing one property override. .EXAMPLE $overrides = ConvertTo-DscPropertyOverrideFromConfig -OverrideConfig $configEntries Converts a list of configuration hashtables into DscPropertyOverride objects. #> function ConvertTo-DscPropertyOverrideFromConfig { [CmdletBinding()] [OutputType([object[]])] param ( [Parameter(Mandatory = $true)] [object[]] $OverrideConfig ) $overrides = [System.Collections.Generic.List[object]]::new() foreach ($entry in $OverrideConfig) { if (-not $entry.ContainsKey('Name') -or [string]::IsNullOrEmpty($entry['Name'])) { Write-Warning 'Skipping a property override entry with a missing or empty Name key.' continue } $overrideParams = @{ Name = [string] $entry['Name'] } if ($entry.ContainsKey('Description') -and -not [string]::IsNullOrEmpty($entry['Description'])) { $overrideParams['Description'] = [string] $entry['Description'] } if ($entry.ContainsKey('Title') -and -not [string]::IsNullOrEmpty($entry['Title'])) { $overrideParams['Title'] = [string] $entry['Title'] } if ($entry.ContainsKey('JsonSchema') -and $null -ne $entry['JsonSchema']) { $overrideParams['JsonSchema'] = $entry['JsonSchema'] } if ($entry.ContainsKey('RemoveKeys') -and $null -ne $entry['RemoveKeys']) { $overrideParams['RemoveKeys'] = @($entry['RemoveKeys']) } if ($entry.ContainsKey('Required') -and $null -ne $entry['Required']) { $overrideParams['Required'] = [bool] $entry['Required'] } $overrides.Add((New-DscPropertyOverride @overrideParams)) } return , $overrides.ToArray() } #EndRegion './Private/ConvertTo-DscPropertyOverrideFromConfig.ps1' 77 #Region './Private/ConvertTo-Hashtable.ps1' -1 <# .SYNOPSIS Recursively converts PSCustomObject and array structures to hashtables. .DESCRIPTION Walks the supplied object and converts every PSCustomObject into an ordered hashtable, every IDictionary into an ordered hashtable, and every IList into an array, recursing into the values. Scalar values are returned unchanged. Useful for normalizing the output of ConvertFrom-Json before consuming it as hashtables. .PARAMETER InputObject The object or structure to recursively convert to a hashtable. .EXAMPLE $parsed = ConvertFrom-Json -InputObject $jsonContent $hashtable = ConvertTo-Hashtable -InputObject $parsed Converts the PSCustomObject graph produced by ConvertFrom-Json into nested ordered hashtables. #> function ConvertTo-Hashtable { [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] [OutputType([System.Object[]])] [OutputType([System.Object])] param ( [Parameter(Mandatory = $true)] [object] $InputObject ) if ($InputObject -is [System.Collections.IDictionary]) { $result = [ordered]@{} foreach ($key in $InputObject.Keys) { $result[$key] = ConvertTo-Hashtable -InputObject $InputObject[$key] } return $result } if ($InputObject -is [PSCustomObject]) { $result = [ordered]@{} foreach ($property in $InputObject.PSObject.Properties) { $result[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value } return $result } if ($InputObject -is [System.Collections.IList]) { $items = [System.Collections.Generic.List[object]]::new() foreach ($item in $InputObject) { $items.Add((ConvertTo-Hashtable -InputObject $item)) } return @($items) } return $InputObject } #EndRegion './Private/ConvertTo-Hashtable.ps1' 67 #Region './Private/ConvertTo-JsonSchemaType.ps1' -1 <# .SYNOPSIS Converts a PowerShell type name to its JSON Schema type definition. .DESCRIPTION Maps a PowerShell type name (such as 'string', 'int', 'bool', 'datetime' or an array form like 'string[]') to a hashtable describing the equivalent JSON Schema type. Unknown types fall back to 'string'. .PARAMETER TypeName The PowerShell type name to convert. .EXAMPLE ConvertTo-JsonSchemaType -TypeName 'bool' Returns @{ type = 'boolean' }. .EXAMPLE ConvertTo-JsonSchemaType -TypeName 'string[]' Returns @{ type = 'array'; items = @{ type = 'string' } }. #> function ConvertTo-JsonSchemaType { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $TypeName ) switch ($TypeName) { 'string' { return @{ type = 'string' } } 'int' { return @{ type = 'integer' } } 'int32' { return @{ type = 'integer' } } 'int64' { return @{ type = 'integer' } } 'long' { return @{ type = 'integer' } } 'double' { return @{ type = 'number' } } 'float' { return @{ type = 'number' } } 'single' { return @{ type = 'number' } } 'decimal' { return @{ type = 'number' } } 'bool' { return @{ type = 'boolean' } } 'boolean' { return @{ type = 'boolean' } } 'switch' { return @{ type = 'boolean' } } 'hashtable' { return @{ type = 'object' } } 'datetime' { return @{ type = 'string'; format = 'date-time' } } default { # arrays like string[] or int[] if ($TypeName -match '^(.+)\[\]$') { $innerType = ConvertTo-JsonSchemaType -TypeName $Matches[1] return @{ type = 'array'; items = $innerType } } # default to string for unknown types return @{ type = 'string' } } } } #EndRegion './Private/ConvertTo-JsonSchemaType.ps1' 63 #Region './Private/Get-ClassCommentBasedHelp.ps1' -1 <# .SYNOPSIS Returns the comment-based help associated with each class in a script. .DESCRIPTION Tokenizes a PowerShell script and locates block comments that immediately precede class declarations (allowing for attributes and blank lines in between). Each matched class name is returned as a key in a hashtable whose value is the parsed comment-based help (Synopsis, Description and Parameters). .PARAMETER Path The full path to a .ps1 or .psm1 file to inspect. .EXAMPLE $helpMap = Get-ClassCommentBasedHelp -Path './MyModule/MyModule.psm1' Returns a hashtable keyed by class name, where each value contains the parsed Synopsis, Description and Parameters from the block comment preceding that class declaration. #> function Get-ClassCommentBasedHelp { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $Path ) [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $null = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) $blockCommentTokens = @($tokens | Where-Object { $_.Kind -eq [System.Management.Automation.Language.TokenKind]::Comment -and $_.Text.StartsWith('<#') }) $classDefinitions = $tokens | Where-Object { $_.Kind -eq [System.Management.Automation.Language.TokenKind]::Class } $result = @{} foreach ($classToken in $classDefinitions) { $classLine = $classToken.Extent.StartLineNumber # Walk backward from the class keyword to find the nearest block comment, # allowing for attributes and blank lines between the comment and class. $nearestComment = $null foreach ($commentToken in $blockCommentTokens) { $gap = $classLine - $commentToken.Extent.EndLineNumber if ($gap -ge 1 -and $gap -le 10) { # Verify no other class keyword exists between this comment and the current class $isValid = $true foreach ($otherClass in $classDefinitions) { if ($otherClass -ne $classToken -and $otherClass.Extent.StartLineNumber -gt $commentToken.Extent.EndLineNumber -and $otherClass.Extent.StartLineNumber -lt $classLine) { $isValid = $false break } } if ($isValid -and ($null -eq $nearestComment -or $commentToken.Extent.EndLineNumber -gt $nearestComment.Extent.EndLineNumber)) { $nearestComment = $commentToken } } } if ($null -eq $nearestComment) { continue } $parsed = ConvertFrom-CommentBasedHelp -CommentText $nearestComment.Text if ($parsed.Synopsis -or $parsed.Description -or $parsed.Parameters.Count -gt 0) { # Determine the class name from the token following 'class' $classIndex = [array]::IndexOf($tokens, $classToken) $className = $null for ($i = $classIndex + 1; $i -lt $tokens.Count; $i++) { if ($tokens[$i].Kind -eq [System.Management.Automation.Language.TokenKind]::Identifier) { $className = $tokens[$i].Text break } } if ($className) { $result[$className] = $parsed } } } return $result } #EndRegion './Private/Get-ClassCommentBasedHelp.ps1' 109 #Region './Private/Get-DscResourceCapability.ps1' -1 <# .SYNOPSIS Returns the DSCv3 capabilities for a class-based DSC resource. .DESCRIPTION Inspects the member AST of a class-based DSC resource type definition and returns the DSCv3 capability strings (such as 'get', 'set', 'test', 'whatIf', 'setHandlesExist', 'delete', 'export') corresponding to the methods implemented on the class. .PARAMETER MemberAst The collection of member AST nodes from the class type definition. .EXAMPLE $capabilities = Get-DscResourceCapability -MemberAst $typeDefinitionAst.Members Returns strings such as 'get', 'set' and 'test' for each DSCv3 method implemented on the class. #> function Get-DscResourceCapability { [CmdletBinding()] [OutputType([string[]])] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.MemberAst[]] $MemberAst ) $capabilities = [System.Collections.Generic.List[string]]::new() $availableMethods = @('get', 'set', 'setHandlesExist', 'whatIf', 'test', 'delete', 'export') $methods = $MemberAst | Where-Object { $_ -is [System.Management.Automation.Language.FunctionMemberAst] -and $_.Name -in $availableMethods } foreach ($method in $methods.Name) { switch ($method) { 'Get' { $capabilities.Add('get') } 'Set' { $capabilities.Add('set') } 'Test' { $capabilities.Add('test') } 'WhatIf' { $capabilities.Add('whatIf') } 'SetHandlesExist' { $capabilities.Add('setHandlesExist') } 'Delete' { $capabilities.Add('delete') } 'Export' { $capabilities.Add('export') } } } return ($capabilities | Select-Object -Unique) } #EndRegion './Private/Get-DscResourceCapability.ps1' 53 #Region './Private/Get-DscResourceProperty.ps1' -1 <# .SYNOPSIS Returns the DSC properties for a class-based DSC resource. .DESCRIPTION Returns a list of hashtables describing each [DscProperty()] decorated property on the supplied class type definition AST, including properties inherited from base classes defined in the same file. .PARAMETER AllTypeDefinitions All type definition AST nodes discovered in the script. Used to resolve base class types and enum types defined in the same file. .PARAMETER TypeDefinitionAst The type definition AST of the class to collect properties from. .EXAMPLE $properties = Get-DscResourceProperty -AllTypeDefinitions $allTypes -TypeDefinitionAst $typeAst Returns a list of hashtables describing every [DscProperty()] decorated property on the class and any base classes defined in the same file. #> function Get-DscResourceProperty { [CmdletBinding()] [OutputType([System.Collections.Generic.List[hashtable]])] [OutputType([System.Object[]])] param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Language.TypeDefinitionAst[]] $AllTypeDefinitions, [Parameter(Mandatory = $true)] [System.Management.Automation.Language.TypeDefinitionAst] $TypeDefinitionAst ) $properties = [System.Collections.Generic.List[hashtable]]::new() Add-AstProperty -AllTypeDefinitions $AllTypeDefinitions -TypeAst $TypeDefinitionAst -Properties $properties return , $properties } #EndRegion './Private/Get-DscResourceProperty.ps1' 43 #Region './Private/Get-DscResourceTypeDefinition.ps1' -1 <# .SYNOPSIS Finds class-based DSC resource type definitions in a PowerShell file. .DESCRIPTION Parses the AST of a PowerShell file and returns the type definitions that are decorated with the [DscResource()] attribute, along with all type definitions discovered in the file (used for resolving base types and enums). .PARAMETER Path The full path to a .ps1 or .psm1 file to parse. .EXAMPLE $dscTypes = Get-DscResourceTypeDefinition -Path './MyModule/MyModule.psm1' Returns a list of hashtables, each containing the TypeDefinitionAst and AllTypeDefinitions for a class decorated with [DscResource()]. #> function Get-DscResourceTypeDefinition { [CmdletBinding()] [OutputType([System.Collections.Generic.List[hashtable]])] param ( [Parameter(Mandatory = $true)] [string] $Path ) [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) foreach ($e in $errors) { Write-Error "Parse error in '$Path': $($e.Message)" } $allTypeDefinitions = $ast.FindAll( { $typeAst = $args[0] -as [System.Management.Automation.Language.TypeDefinitionAst] return $null -ne $typeAst }, $false ) $results = [System.Collections.Generic.List[hashtable]]::new() foreach ($typeDefinition in $allTypeDefinitions) { foreach ($attribute in $typeDefinition.Attributes) { if ($attribute.TypeName.Name -eq 'DscResource') { $results.Add(@{ TypeDefinitionAst = $typeDefinition AllTypeDefinitions = $allTypeDefinitions }) break } } } return $results } #EndRegion './Private/Get-DscResourceTypeDefinition.ps1' 66 #Region './Private/New-EmbeddedJsonSchema.ps1' -1 <# .SYNOPSIS Builds the embedded JSON schema for a class-based DSC resource. .DESCRIPTION Produces an ordered hashtable representing the embedded JSON Schema document for an adapted resource manifest. The schema describes the DSC resource properties and their required-ness, and uses descriptions from the supplied class comment-based help when available. .PARAMETER ResourceName The fully-qualified resource type name (for example 'MyModule/MyResource') used as the schema title. .PARAMETER Properties The list of property hashtables produced by Get-DscResourceProperty. .PARAMETER Description Optional description to embed in the schema document. .PARAMETER ClassHelp Optional hashtable produced by Get-ClassCommentBasedHelp containing per-parameter descriptions to use for property descriptions. .PARAMETER AllowNonEcmaPattern When specified, `[ValidatePattern()]` values containing .NET-specific regex constructs (such as `\A`, `\Z`, atomic groups, or inline flags) are still emitted as the JSON Schema `pattern` keyword even though they may not be understood by ECMA 262 validators. By default such patterns are silently skipped and a warning is written. .EXAMPLE $schema = New-EmbeddedJsonSchema -ResourceName 'MyModule/MyResource' -Properties $properties Builds an embedded JSON Schema document for the given resource and property list using default descriptions. .EXAMPLE $schema = New-EmbeddedJsonSchema -ResourceName 'MyModule/MyResource' ` -Properties $properties -Description 'Manages my resource.' -ClassHelp $helpMap Builds the schema using descriptions sourced from the class comment-based help. #> function New-EmbeddedJsonSchema { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [Parameter(Mandatory = $true)] [string] $ResourceName, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[hashtable]] $Properties, [Parameter()] [string] $Description, [Parameter()] [hashtable] $ClassHelp, [Parameter()] [System.Management.Automation.SwitchParameter] $AllowNonEcmaPattern ) $schemaProperties = [ordered]@{} $requiredList = [System.Collections.Generic.List[string]]::new() foreach ($prop in $Properties) { $schemaProp = [ordered]@{} if ($prop.EnumValues) { $schemaProp['type'] = 'string' $schemaProp['enum'] = $prop.EnumValues } else { $jsonType = ConvertTo-JsonSchemaType -TypeName $prop.TypeName foreach ($key in $jsonType.Keys) { $schemaProp[$key] = $jsonType[$key] } } $schemaProp['title'] = $prop.Name if (-not $prop.EnumValues -and $prop.PatternValue) { if ($AllowNonEcmaPattern -or (Test-IsEcmaCompatiblePattern -Pattern $prop.PatternValue)) { $schemaProp['pattern'] = $prop.PatternValue } else { Write-Warning "Property '$($prop.Name)': ValidatePattern value contains .NET-specific regex constructs that are not ECMA 262 compatible and will not be emitted. Use -AllowNonEcmaPattern to override." } } if ($prop.IsNotConfigurable) { $schemaProp['readOnly'] = $true } if ($ClassHelp -and $ClassHelp.Parameters.ContainsKey($prop.Name)) { $schemaProp['description'] = $ClassHelp.Parameters[$prop.Name] } else { $schemaProp['description'] = "The $($prop.Name) property." } $schemaProperties[$prop.Name] = $schemaProp if ($prop.IsMandatory) { $requiredList.Add($prop.Name) } } $schema = [ordered]@{ '$schema' = $script:JsonSchemaUri title = $ResourceName type = 'object' required = @($requiredList) additionalProperties = $false properties = $schemaProperties } if (-not [string]::IsNullOrEmpty($Description)) { $schema['description'] = $Description } if ($PSCmdlet.ShouldProcess($ResourceName, 'Create embedded JSON schema')) { return $schema } } #EndRegion './Private/New-EmbeddedJsonSchema.ps1' 148 #Region './Private/Resolve-ModuleInfo.ps1' -1 <# .SYNOPSIS Resolves module metadata from a .psd1, .psm1 or .ps1 file. .DESCRIPTION Returns a hashtable containing the module name, version, author, description and the path to the script file that should be parsed for DSC resources. When a .psd1 path is provided the module manifest is imported and the RootModule is resolved relative to the manifest's directory. When a .ps1 or .psm1 is provided, a sibling .psd1 is used when present; otherwise default values are returned. .PARAMETER Path The path to a .ps1, .psm1 or .psd1 file. .EXAMPLE $info = Resolve-ModuleInfo -Path './MyModule/MyModule.psd1' Returns a hashtable with ModuleName, Version, Author, Description, ScriptPath, Psd1Path and Directory populated from the module manifest. .EXAMPLE $info = Resolve-ModuleInfo -Path './MyResource.psm1' Returns a hashtable with defaults when no companion .psd1 exists. #> function Resolve-ModuleInfo { [CmdletBinding()] [OutputType([hashtable])] param ( [Parameter(Mandatory = $true)] [string] $Path ) $resolvedPath = Resolve-Path -LiteralPath $Path $extension = [System.IO.Path]::GetExtension($resolvedPath) $directory = [System.IO.Path]::GetDirectoryName($resolvedPath) if ($extension -eq '.psd1') { $manifestData = Import-PowerShellDataFile -Path $resolvedPath $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath) $version = if ($manifestData.ModuleVersion) { $manifestData.ModuleVersion } else { '0.0.1' } $author = if ($manifestData.Author) { $manifestData.Author } else { '' } $description = if ($manifestData.Description) { ($manifestData.Description -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join ' ' } else { '' } $rootModule = $manifestData.RootModule if ([string]::IsNullOrEmpty($rootModule)) { $rootModule = "$moduleName.psm1" } $scriptPath = Join-Path $directory $rootModule $psd1RelativePath = [System.IO.Path]::GetFileName($resolvedPath) return @{ ModuleName = $moduleName Version = $version Author = $author Description = $description ScriptPath = $scriptPath Psd1Path = $psd1RelativePath Directory = $directory } } # derive fileName from .ps1 or .psm1 $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath) # validate if .psd1 is there and use that $psd1Path = Join-Path $directory "$moduleName.psd1" if (Test-Path -LiteralPath $psd1Path) { return Resolve-ModuleInfo -Path $psd1Path } $fileName = [System.IO.Path]::GetFileName($resolvedPath) return @{ ModuleName = $moduleName Version = '0.0.1' Author = '' Description = '' ScriptPath = [string]$resolvedPath Psd1Path = $fileName Directory = $directory } } #EndRegion './Private/Resolve-ModuleInfo.ps1' 91 #Region './Private/Test-IsEcmaCompatiblePattern.ps1' -1 <# .SYNOPSIS Tests whether a regex pattern is compatible with the ECMA 262 dialect used by JSON Schema validators. .DESCRIPTION Inspects a regex string for .NET-specific constructs that have no equivalent in ECMA 262. When any such construct is found the function returns $false so callers can decide whether to emit the pattern as a JSON Schema `pattern` keyword. The following .NET-only constructs are detected: * `\A`, `\Z`, `\z` - .NET position anchors (ECMA uses `^`/`$` only). * `(?>...)` - atomic (possessive) groups. * `(?#...)` - inline comments. * `(?imnsx)` / `(?i:...)` - inline option flags. * `(?<name-name>...)` - balancing groups. Note: positive/negative lookbehind (`(?<=`, `(?<!`) and Unicode property escapes (`\p{L}`) are intentionally *not* flagged because ECMA 2018 (supported by most modern JSON Schema validators) includes them. .PARAMETER Pattern The regex pattern string to evaluate. .EXAMPLE Test-IsEcmaCompatiblePattern -Pattern '^[a-z]+$' Returns $true — the pattern uses only portable syntax. .EXAMPLE Test-IsEcmaCompatiblePattern -Pattern '^\A[a-z]+\Z$' Returns $false — `\A` and `\Z` are .NET-only anchors. #> function Test-IsEcmaCompatiblePattern { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string] $Pattern ) # Each entry is a regex that matches a .NET-specific construct. $dotNetConstructs = @( '\\[AZz]' # .NET anchors: \A, \Z, \z '\(\?>' # atomic groups: (?>...) '\(\?#' # inline comments: (?#...) '\(\?[imnsx]+' # inline option flags: (?i), (?ix:...) etc. '\(\?<\w+-\w+>' # balancing groups: (?<open-close>...) ) foreach ($construct in $dotNetConstructs) { if ($Pattern -match $construct) { return $false } } return $true } #EndRegion './Private/Test-IsEcmaCompatiblePattern.ps1' 67 #Region './Public/Import-DscAdaptedResourceManifest.ps1' -1 <# .SYNOPSIS Imports adapted resource manifest objects from `.dsc.adaptedResource.json` files. .DESCRIPTION Reads one or more `.dsc.adaptedResource.json` files and returns DscAdaptedResourceManifest objects. This is the inverse of serializing a manifest with `.ToJson()` - it allows you to load existing adapted resource manifests for inspection, modification, or inclusion in a resource manifest list via New-DscResourceManifest. .PARAMETER Path The path to a `.dsc.adaptedResource.json` file. Accepts pipeline input. .EXAMPLE Import-DscAdaptedResourceManifest -Path ./MyResource.dsc.adaptedResource.json Imports a single adapted resource manifest and returns a DscAdaptedResourceManifest object. .EXAMPLE Get-ChildItem -Filter *.dsc.adaptedResource.json | Import-DscAdaptedResourceManifest Imports all adapted resource manifest files in the current directory. .EXAMPLE Import-DscAdaptedResourceManifest -Path ./MyResource.dsc.adaptedResource.json | New-DscResourceManifest Imports an adapted resource manifest and bundles it into a resource manifest list. .OUTPUTS Returns a DscAdaptedResourceManifest object for each file. The object has .ToJson() and .ToHashtable() methods for serialization. #> function Import-DscAdaptedResourceManifest { [CmdletBinding()] [OutputType([DscAdaptedResourceManifest])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateScript({ if (-not (Test-Path -LiteralPath $_)) { throw "Path '$_' does not exist." } return $true })] [Alias('FullName')] [string] $Path ) process { $resolvedPath = Resolve-Path -LiteralPath $Path Write-Verbose "Importing adapted resource manifest from '$resolvedPath'" $jsonContent = Get-Content -LiteralPath $resolvedPath -Raw $parsed = ConvertFrom-Json -InputObject $jsonContent $hashtable = ConvertTo-Hashtable -InputObject $parsed $manifest = ConvertTo-AdaptedResourceManifest -Hashtable $hashtable Write-Output $manifest } } #EndRegion './Public/Import-DscAdaptedResourceManifest.ps1' 66 #Region './Public/Import-DscResourceManifest.ps1' -1 <# .SYNOPSIS Imports a DSC resource manifest list from a `.dsc.manifests.json` file. .DESCRIPTION Reads a `.dsc.manifests.json` file and returns a DscResourceManifestList object containing the adapted resources, command-based resources, and extensions defined in the file. This is the inverse of serializing a manifest list with `.ToJson()`. The adapted resources in the returned list are hydrated into DscAdaptedResourceManifest objects and stored via AddAdaptedResource. Resources and extensions are stored as hashtables. .PARAMETER Path The path to a `.dsc.manifests.json` file. Accepts pipeline input. .EXAMPLE Import-DscResourceManifest -Path ./MyModule.dsc.manifests.json Imports a manifest list file and returns a DscResourceManifestList object. .EXAMPLE Get-ChildItem -Filter *.dsc.manifests.json | Import-DscResourceManifest Imports all manifest list files in the current directory. .EXAMPLE $list = Import-DscResourceManifest -Path ./existing.dsc.manifests.json $list.AdaptedResources.Count Imports a manifest list and inspects the number of adapted resources. .OUTPUTS Returns a DscResourceManifestList object with .ToJson() for serialization. #> function Import-DscResourceManifest { [CmdletBinding()] [OutputType([DscResourceManifestList])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateScript({ if (-not (Test-Path -LiteralPath $_)) { throw "Path '$_' does not exist." } return $true })] [Alias('FullName')] [string] $Path ) process { $resolvedPath = Resolve-Path -LiteralPath $Path Write-Verbose "Importing resource manifest list from '$resolvedPath'" $jsonContent = Get-Content -LiteralPath $resolvedPath -Raw $parsed = ConvertFrom-Json -InputObject $jsonContent $hashtable = ConvertTo-Hashtable -InputObject $parsed $manifestList = [DscResourceManifestList]::new() if ($hashtable.Contains('adaptedResources')) { foreach ($ar in $hashtable['adaptedResources']) { $manifest = ConvertTo-AdaptedResourceManifest -Hashtable $ar $manifestList.AddAdaptedResource($manifest) } } if ($hashtable.Contains('resources')) { foreach ($res in $hashtable['resources']) { $manifestList.AddResource($res) } } if ($hashtable.Contains('extensions')) { foreach ($ext in $hashtable['extensions']) { $manifestList.AddExtension($ext) } } Write-Output $manifestList } } #EndRegion './Public/Import-DscResourceManifest.ps1' 94 #Region './Public/New-DscAdaptedResourceManifest.ps1' -1 <# .SYNOPSIS Creates adapted resource manifest objects from class-based PowerShell DSC resources. .DESCRIPTION Parses the AST of a PowerShell file (.ps1, .psm1, or .psd1) to find class-based DSC resources decorated with the [DscResource()] attribute. For each resource found, it returns a DscAdaptedResourceManifest object that complies with the DSCv3 adapted resource manifest JSON schema. The returned objects can be serialized to JSON using the .ToJson() method and written to `.dsc.adaptedResource.json` files. These manifests enable DSCv3 to discover and use PowerShell DSC resources without running Invoke-DscCacheRefresh. .PARAMETER Path The path to a .ps1, .psm1, or .psd1 file containing class-based DSC resources. When a .psd1 is provided, the RootModule is resolved and parsed automatically. If no .psd1 is available (e.g. a standalone .ps1 or .psm1 without a sibling manifest), the version defaults to '0.0.1'. Use the Version parameter to supply the correct version in that case. .PARAMETER Version Overrides the version resolved from the module manifest. Must be a valid semantic version string (e.g. '1.2.3' or '1.2.3-preview'). When omitted, the version from the .psd1 ModuleVersion field is used, or '0.0.1' for files without a co-located manifest. .EXAMPLE New-DscAdaptedResourceManifest -Path ./MyModule/MyModule.psd1 Returns adapted resource manifest objects for all class-based DSC resources in the module. .EXAMPLE New-DscAdaptedResourceManifest -Path ./MyResource.ps1 | ForEach-Object { $_.ToJson() | Set-Content "$($_.Type -replace '/', '.').dsc.adaptedResource.json" } Generates manifest objects and writes each to a JSON file. .EXAMPLE Get-ChildItem -Path ./MyModules -Filter *.psd1 -Recurse | New-DscAdaptedResourceManifest Discovers all module manifests under `./MyModules` and pipes them into the function to generate adapted resource manifests for every class-based DSC resource found. .OUTPUTS Returns a DscAdaptedResourceManifest object for each class-based DSC resource found. The object has a .ToJson() method for serialization to the adapted resource manifest JSON format. .PARAMETER AllowNonEcmaPattern When specified, `[ValidatePattern()]` regex values that contain .NET-specific constructs incompatible with ECMA 262 (e.g. `\A`, `\Z`, atomic groups, inline flags) are still written into the JSON Schema `pattern` keyword. By default such patterns are skipped and a warning is written instead. #> function New-DscAdaptedResourceManifest { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [OutputType([DscAdaptedResourceManifest])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [ValidateScript({ if (-not (Test-Path -LiteralPath $_)) { throw "Path '$_' does not exist." } $ext = [System.IO.Path]::GetExtension($_) if ($ext -notin '.ps1', '.psm1', '.psd1') { throw "Path '$_' must be a .ps1, .psm1, or .psd1 file." } return $true })] [string] $Path, # Semantic version string for PS7: SemanticVersion Class [Parameter()] [ValidateScript({ if ($_ -notmatch '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$') { throw "Version '$_' is not a valid semantic version (e.g. '1.2.3' or '1.2.3-preview')." } return $true })] [string] $Version, [Parameter()] [System.Management.Automation.SwitchParameter] $AllowNonEcmaPattern ) process { $moduleInfo = Resolve-ModuleInfo -Path $Path if (-not (Test-Path -LiteralPath $moduleInfo.ScriptPath)) { Write-Error "Cannot find script file '$($moduleInfo.ScriptPath)' to parse." return } $dscTypes = Get-DscResourceTypeDefinition -Path $moduleInfo.ScriptPath if ($dscTypes.Count -eq 0) { Write-Warning "No class-based DSC resources found in '$Path'." return } $classHelpMap = Get-ClassCommentBasedHelp -Path $moduleInfo.ScriptPath foreach ($entry in $dscTypes) { $typeDefinitionAst = $entry.TypeDefinitionAst $allTypeDefinitions = $entry.AllTypeDefinitions $resourceName = $typeDefinitionAst.Name $resourceType = "$($moduleInfo.ModuleName)/$resourceName" Write-Verbose "Processing DSC resource '$resourceType'" $capabilities = Get-DscResourceCapability -MemberAst $typeDefinitionAst.Members $properties = Get-DscResourceProperty -AllTypeDefinitions $allTypeDefinitions -TypeDefinitionAst $typeDefinitionAst $classHelp = $null $resourceDescription = $moduleInfo.Description if ($classHelpMap.ContainsKey($resourceName)) { $classHelp = $classHelpMap[$resourceName] if (-not [string]::IsNullOrWhiteSpace($classHelp.Synopsis)) { $resourceDescription = $classHelp.Synopsis } elseif (-not [string]::IsNullOrWhiteSpace($classHelp.Description)) { $resourceDescription = $classHelp.Description } $missingParams = @() foreach ($prop in $properties) { if (-not $classHelp.Parameters.ContainsKey($prop.Name)) { $missingParams += $prop.Name } } if ($missingParams.Count -gt 0) { Write-Warning "Class '$resourceName' comment-based help is missing .PARAMETER documentation for: $($missingParams -join ', ')" } } else { Write-Warning "No comment-based help found above class '$resourceName'. Using default descriptions." } $newEmbeddedJsonSchemaParameters = @{ ResourceName = $resourceType Properties = $properties Description = $resourceDescription ClassHelp = $classHelp AllowNonEcmaPattern = $AllowNonEcmaPattern } $embeddedSchema = New-EmbeddedJsonSchema @newEmbeddedJsonSchemaParameters $manifest = [DscAdaptedResourceManifest]::new() $manifest.Schema = $script:AdaptedResourceSchemaUri $manifest.Type = $resourceType $manifest.Kind = 'resource' $manifest.Version = if ($PSBoundParameters.ContainsKey('Version')) { $Version } else { $moduleInfo.Version } $manifest.Capabilities = @($capabilities) $manifest.Description = $resourceDescription $manifest.Author = $moduleInfo.Author $manifest.RequireAdapter = $script:DefaultAdapter $manifest.Path = $moduleInfo.Psd1Path $manifest.ManifestSchema = [DscAdaptedResourceManifestSchema]@{ Embedded = $embeddedSchema } if ($PSCmdlet.ShouldProcess($resourceType, 'Create adapted resource manifest')) { Write-Output $manifest } } } } #EndRegion './Public/New-DscAdaptedResourceManifest.ps1' 194 #Region './Public/New-DscPropertyOverride.ps1' -1 <# .SYNOPSIS Creates a DscPropertyOverride object for use with Update-DscAdaptedResourceManifest. .DESCRIPTION Constructs a DscPropertyOverride object that specifies how to modify a single property in the embedded JSON schema of an adapted resource manifest. .PARAMETER Name The name of the property in the embedded JSON schema to override. .PARAMETER Description Override the property description text. .PARAMETER Title Override the property title text. .PARAMETER JsonSchema A hashtable of JSON schema keywords to merge into the property definition (e.g., anyOf, oneOf, default, minimum, maximum, pattern, format). .PARAMETER RemoveKeys An array of JSON schema key names to remove from the property before merging JsonSchema (e.g., 'type', 'enum' when replacing with anyOf). .PARAMETER Required Set to $true to add the property to the required list, $false to remove it, or omit to leave unchanged. .EXAMPLE New-DscPropertyOverride -Name 'Enabled' -Description 'Whether this resource is active.' Creates an override that sets a custom description for the Enabled property. .EXAMPLE New-DscPropertyOverride -Name 'Status' -RemoveKeys 'type','enum' -JsonSchema @{ anyOf = @( @{ type = 'string'; enum = @('Active', 'Inactive') } @{ type = 'integer'; minimum = 0 } ) } Creates an override that replaces the type/enum with an anyOf schema. .EXAMPLE $overrides = @( New-DscPropertyOverride -Name 'Name' -Description 'The unique identifier.' New-DscPropertyOverride -Name 'Count' -JsonSchema @{ minimum = 0; maximum = 100 } ) $manifest | Update-DscAdaptedResourceManifest -PropertyOverride $overrides Creates multiple overrides and pipes them to Update-DscAdaptedResourceManifest. .OUTPUTS Returns a DscPropertyOverride object. #> function New-DscPropertyOverride { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [OutputType([DscPropertyOverride])] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [string] $Description, [Parameter()] [string] $Title, [Parameter()] [hashtable] $JsonSchema, [Parameter()] [string[]] $RemoveKeys, [Parameter()] [nullable[bool]] $Required ) $override = [DscPropertyOverride]::new() $override.Name = $Name if ($PSBoundParameters.ContainsKey('Description')) { $override.Description = $Description } if ($PSBoundParameters.ContainsKey('Title')) { $override.Title = $Title } if ($PSBoundParameters.ContainsKey('JsonSchema')) { $override.JsonSchema = $JsonSchema } if ($PSBoundParameters.ContainsKey('RemoveKeys')) { $override.RemoveKeys = $RemoveKeys } if ($PSBoundParameters.ContainsKey('Required')) { $override.Required = $Required } if ($PSCmdlet.ShouldProcess($Name, 'Overwrite property in adapted resource manifest')) { Write-Output $override } } #EndRegion './Public/New-DscPropertyOverride.ps1' 121 #Region './Public/New-DscResourceManifest.ps1' -1 <# .SYNOPSIS Creates a DSC resource manifests list for bundling multiple resources in a single file. .DESCRIPTION Builds a DscResourceManifestList object that can contain both adapted resources and command-based resources. The resulting object can be serialized to JSON and written to a `.dsc.manifests.json` file, which DSCv3 discovers and loads as a bundle. Adapted resources can be added by piping DscAdaptedResourceManifest objects from New-DscAdaptedResourceManifest. Command-based resources can be added via the -Resource parameter as hashtables matching the DSCv3 resource manifest schema. .PARAMETER AdaptedResource One or more DscAdaptedResourceManifest objects to include in the manifests list. These are typically produced by New-DscAdaptedResourceManifest. .PARAMETER Resource One or more hashtables representing command-based DSC resource manifests. Each hashtable should conform to the DSCv3 resource manifest schema with keys such as `$schema`, `type`, `version`, `get`, `set`, `test`, `schema`, etc. .EXAMPLE $adapted = New-DscAdaptedResourceManifest -Path ./MyModule/MyModule.psd1 New-DscResourceManifest -AdaptedResource $adapted Creates a manifests list from adapted resource manifests generated from a module. .EXAMPLE $resource = @{ '$schema' = 'https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json' type = 'MyCompany/MyTool' version = '1.0.0' get = @{ executable = 'mytool'; args = @('get') } set = @{ executable = 'mytool'; args = @('set'); implementsPretest = $false; return = 'state' } test = @{ executable = 'mytool'; args = @('test'); return = 'state' } exitCodes = @{ '0' = 'Success'; '1' = 'Error' } schema = @{ command = @{ executable = 'mytool'; args = @('schema') } } } New-DscResourceManifest -Resource $resource Creates a manifests list containing a single command-based resource. .EXAMPLE New-DscAdaptedResourceManifest -Path ./MyModule/MyModule.psd1 | New-DscResourceManifest Pipes adapted resource manifests directly into the function via the pipeline. .OUTPUTS Returns a DscResourceManifestList object with a .ToJson() method for serialization to the `.dsc.manifests.json` format. #> function New-DscResourceManifest { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [OutputType([DscResourceManifestList])] param ( [Parameter(ValueFromPipeline = $true)] [DscAdaptedResourceManifest[]] $AdaptedResource, [Parameter()] [hashtable[]] $Resource ) begin { $manifestList = [DscResourceManifestList]::new() if ($Resource) { foreach ($res in $Resource) { $manifestList.AddResource($res) } } } process { if ($AdaptedResource) { foreach ($adapted in $AdaptedResource) { $manifestList.AddAdaptedResource($adapted) } } } end { if ($PSCmdlet.ShouldProcess('DscResourceManifest', 'Create')) { Write-Output $manifestList } } } #EndRegion './Public/New-DscResourceManifest.ps1' 100 #Region './Public/Task.Create_DscAdaptedResourceManifests.ps1' -1 <# .SYNOPSIS This is the alias to the build task Create_DscAdaptedResourceManifests's script file. .DESCRIPTION This makes available the alias 'Task.Create_DscAdaptedResourceManifests' that is exported in the module manifest so that the build task can be correctly imported using for example Invoke-Build. .NOTES This is using the pattern lined out in the Invoke-Build repository https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import. #> Set-Alias -Name 'Task.Create_DscAdaptedResourceManifests' -Value "$PSScriptRoot/tasks/Create_DscAdaptedResourceManifests.build.ps1" #EndRegion './Public/Task.Create_DscAdaptedResourceManifests.ps1' 17 #Region './Public/Task.Create_DscResourceManifestsList.ps1' -1 <# .SYNOPSIS This is the alias to the build task Create_DscResourceManifestsList's script file. .DESCRIPTION This makes available the alias 'Task.Create_DscResourceManifestsList that is exported in the module manifest so that the build task can be correctly imported using for example Invoke-Build. .NOTES This is using the pattern lined out in the Invoke-Build repository https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import. #> Set-Alias -Name 'Task.Create_DscResourceManifestsList' -Value "$PSScriptRoot/tasks/Create_DscResourceManifestsList.build.ps1" #EndRegion './Public/Task.Create_DscResourceManifestsList.ps1' 17 #Region './Public/Update-DscAdaptedResourceManifest.ps1' -1 <# .SYNOPSIS Applies post-processing overrides to adapted resource manifest objects. .DESCRIPTION Modifies the embedded JSON schema of a DscAdaptedResourceManifest object by applying property-level overrides. This enables customization that AST extraction alone cannot provide, such as meaningful property descriptions, JSON schema keywords like anyOf or oneOf for complex type unions, default values, numeric ranges, and string patterns. Property overrides are specified via DscPropertyOverride objects that target individual properties by name. Each override can change the description, title, required status, remove existing JSON schema keys, and merge in new JSON schema keywords. .PARAMETER InputObject A DscAdaptedResourceManifest object to update. Typically produced by New-DscAdaptedResourceManifest. Accepts pipeline input. .PARAMETER PropertyOverride One or more DscPropertyOverride objects specifying modifications to individual properties in the embedded JSON schema. Each override targets a property by Name. DscPropertyOverride supports the following fields: - Name: (Required) The property name to modify. - Description: Override the property description. - Title: Override the property title. - JsonSchema: A hashtable of JSON schema keywords to merge into the property (e.g., anyOf, oneOf, default, minimum, maximum, pattern, format). - RemoveKeys: An array of JSON schema key names to remove before merging (e.g., 'type', 'enum' when replacing with anyOf). - Required: Set to $true to mark as required, $false to remove from required, or leave $null to keep unchanged. .PARAMETER Description Override the resource-level description on both the manifest object and the embedded JSON schema. .EXAMPLE New-DscAdaptedResourceManifest -Path ./MyModule/MyModule.psd1 | Update-DscAdaptedResourceManifest -PropertyOverride @( [DscPropertyOverride]@{ Name = 'Name' Description = 'The unique name identifying this resource instance.' } ) Overrides the auto-generated description for the Name property. .EXAMPLE $overrides = @( [DscPropertyOverride]@{ Name = 'Status' Description = 'The desired status, as a label or numeric code.' RemoveKeys = @('type', 'enum') JsonSchema = @{ anyOf = @( @{ type = 'string'; enum = @('Active', 'Inactive') } @{ type = 'integer'; minimum = 0 } ) } } ) New-DscAdaptedResourceManifest -Path ./MyModule.psd1 | Update-DscAdaptedResourceManifest -PropertyOverride $overrides Replaces a simple enum property with an anyOf schema allowing either a string enum or an integer value. .EXAMPLE $override = [DscPropertyOverride]@{ Name = 'Count' JsonSchema = @{ minimum = 0; maximum = 100; default = 1 } } $manifest | Update-DscAdaptedResourceManifest -PropertyOverride $override Adds numeric constraints and a default value to an existing integer property. .EXAMPLE $override = [DscPropertyOverride]@{ Name = 'Tags' Required = $false } $manifest | Update-DscAdaptedResourceManifest -PropertyOverride $override Removes a property from the required list. .OUTPUTS Returns the modified DscAdaptedResourceManifest object. #> function Update-DscAdaptedResourceManifest { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [OutputType([DscAdaptedResourceManifest])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [DscAdaptedResourceManifest] $InputObject, [Parameter()] [DscPropertyOverride[]] $PropertyOverride, [Parameter()] [string] $Description ) process { $schema = $InputObject.ManifestSchema.Embedded if (-not [string]::IsNullOrEmpty($Description)) { $InputObject.Description = $Description if ($schema.Contains('description')) { $schema['description'] = $Description } } if ($PropertyOverride) { $properties = $schema['properties'] $requiredList = [System.Collections.Generic.List[string]]::new() if ($schema.Contains('required') -and $null -ne $schema['required']) { foreach ($r in $schema['required']) { $requiredList.Add($r) } } foreach ($override in $PropertyOverride) { if (-not $properties.Contains($override.Name)) { Write-Warning "Property '$($override.Name)' not found in schema for '$($InputObject.Type)'. Skipping." continue } $prop = $properties[$override.Name] # Remove specified keys first if ($override.RemoveKeys) { foreach ($key in $override.RemoveKeys) { if ($prop.Contains($key)) { $prop.Remove($key) } } } # Apply description override if (-not [string]::IsNullOrEmpty($override.Description)) { $prop['description'] = $override.Description } # Apply title override if (-not [string]::IsNullOrEmpty($override.Title)) { $prop['title'] = $override.Title } # Merge JSON schema keywords if ($override.JsonSchema -and $override.JsonSchema.Count -gt 0) { foreach ($key in $override.JsonSchema.Keys) { $prop[$key] = $override.JsonSchema[$key] } } # Handle required override if ($null -ne $override.Required) { if ([bool]$override.Required -and $override.Name -notin $requiredList) { $requiredList.Add($override.Name) } elseif (-not [bool]$override.Required) { $requiredList.Remove($override.Name) | Out-Null } } } $schema['required'] = @($requiredList) } if ($PSCmdlet.ShouldProcess($InputObject.Type, 'Update adapted resource manifest')) { Write-Output $InputObject } } } #EndRegion './Public/Update-DscAdaptedResourceManifest.ps1' 200 |