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
        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 (-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 })
        }

        $Properties.Add(@{
                Name              = $propertyAst.Name
                TypeName          = $typeName
                IsKey             = $isKey
                IsMandatory       = $isMandatory -or $isKey
                IsNotConfigurable = $isNotConfigurable
                EnumValues        = $enumValues
            })
    }
}
#EndRegion './Private/Add-AstProperty.ps1' 120
#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-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.

    .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
    )

    $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 ($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' 125
#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 './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.
#>

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
    )

    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."
            }

            $embeddedSchema = New-EmbeddedJsonSchema -ResourceName $resourceType -Properties $properties -Description $resourceDescription -ClassHelp $classHelp

            $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' 176
#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/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