DSCSchemaGenerator.psm1

# DSCSchemaGenerator.psm1
# PowerShell module for generating DSC resource manifests with JSON schemas from class-based resources
# Architecture: SOLID-compliant with dependency injection

#region Module Initialization

$script:ModulePath = $PSScriptRoot

# Shared JSON serialization options
$script:JsonOptions = [System.Text.Json.JsonSerializerOptions]::new()
$script:JsonOptions.WriteIndented = $true

#endregion

#region Private Helper Functions

function New-ManifestGenerator {
    <#
    .SYNOPSIS
        Creates a new manifest generator with all dependencies injected.
    #>

    [OutputType([DSCSchemaGenerator.Services.DscResourceManifestGenerator])]
    param()

    $typeMapper = [DSCSchemaGenerator.Services.JsonSchemaTypeMapper]::new()
    $schemaGenerator = [DSCSchemaGenerator.Services.DscJsonSchemaGenerator]::new($typeMapper)
    $operationBuilder = [DSCSchemaGenerator.Services.PowerShellOperationBuilder]::new()
    
    return [DSCSchemaGenerator.Services.DscResourceManifestGenerator]::new($schemaGenerator, $operationBuilder)
}

function New-SchemaGenerator {
    <#
    .SYNOPSIS
        Creates a new schema generator with dependencies injected.
    #>

    [OutputType([DSCSchemaGenerator.Services.DscJsonSchemaGenerator])]
    param()

    $typeMapper = [DSCSchemaGenerator.Services.JsonSchemaTypeMapper]::new()
    return [DSCSchemaGenerator.Services.DscJsonSchemaGenerator]::new($typeMapper)
}

function New-ManifestOptions {
    <#
    .SYNOPSIS
        Creates manifest options from parameters.
    #>

    [OutputType([DSCSchemaGenerator.Models.ManifestOptions])]
    param(
        [string]$Executable = 'pwsh',
        [string]$ScriptPath = './resource.ps1',
        [string]$ResourceTypePrefix = '',
        [string]$Version = '0.0.1',
        [string[]]$Tags = @(),
        [string]$SetReturn = $null,
        [string]$TestReturn = $null,
        [string]$ScriptArgs = $null
    )

    return [DSCSchemaGenerator.Models.ManifestOptions]@{
        Executable         = $Executable
        ScriptPath         = $ScriptPath
        ResourceTypePrefix = $ResourceTypePrefix
        Version            = $Version
        Tags               = $Tags
        SetReturn          = $SetReturn
        TestReturn         = $TestReturn
        ScriptArgs         = $ScriptArgs
    }
}

function Write-FileContent {
    <#
    .SYNOPSIS
        Writes content to a file, creating directories if needed.
    #>

    param(
        [Parameter(Mandatory)]
        [string]$Path,
        
        [Parameter(Mandatory)]
        [string]$Content
    )

    $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
    $directory = [System.IO.Path]::GetDirectoryName($resolvedPath)
    
    if (-not (Test-Path $directory)) {
        New-Item -Path $directory -ItemType Directory -Force | Out-Null
    }
    
    [System.IO.File]::WriteAllText($resolvedPath, $Content)
    Write-Verbose "Written to: $resolvedPath"
}

#endregion

#region Public Functions - File-based (parse .psm1 files)

function Read-DscResourceModule {
    <#
    .SYNOPSIS
        Parses a PowerShell module or script file and extracts DSC resource information.
 
    .DESCRIPTION
        Uses the PowerShell AST to parse a .psm1 or .ps1 file and extract information about
        DSC resource classes, enums, and helper classes.
 
    .PARAMETER ModuleFile
        Path to a PowerShell module file (.psm1).
 
    .PARAMETER ScriptFile
        Path to a PowerShell script file (.ps1).
 
    .OUTPUTS
        DSCSchemaGenerator.Models.DscModuleInfo
 
    .EXAMPLE
        $moduleInfo = Read-DscResourceModule -ModuleFile './MyResources.psm1'
        $moduleInfo.Resources | ForEach-Object { $_.ClassName }
 
    .EXAMPLE
        $moduleInfo = Read-DscResourceModule -ScriptFile './MyResource.ps1'
    #>

    [CmdletBinding(DefaultParameterSetName = 'ModuleFile')]
    [OutputType([DSCSchemaGenerator.Models.DscModuleInfo])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ModuleFile', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName', 'PSPath')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.psm1$') { throw "ModuleFile must be a .psm1 file: $_" }
            $true
        })]
        [string]$ModuleFile,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ScriptFile', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.ps1$') { throw "ScriptFile must be a .ps1 file: $_" }
            $true
        })]
        [string]$ScriptFile
    )

    process {
        $filePath = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') { $ModuleFile } else { $ScriptFile }
        $resolvedPath = Resolve-Path $filePath -ErrorAction Stop
        
        $parser = [DSCSchemaGenerator.Services.PowerShellModuleParser]::new()
        $moduleInfo = $parser.ParseFile($resolvedPath.Path)

        foreach ($parseError in $moduleInfo.Errors) {
            Write-Warning "Parse error: $parseError"
        }

        return $moduleInfo
    }
}

function New-DscSchemaFromFile {
    <#
    .SYNOPSIS
        Generates JSON Schema from a PowerShell module or script file.
 
    .DESCRIPTION
        Parses a .psm1 or .ps1 file and generates JSON Schema for DSC resource classes.
        Works directly with PowerShell class definitions without compilation.
 
    .PARAMETER ModuleFile
        Path to a PowerShell module file (.psm1).
 
    .PARAMETER ScriptFile
        Path to a PowerShell script file (.ps1).
 
    .PARAMETER ResourceName
        Name of a specific DSC resource to generate schema for.
        If omitted, generates schemas for all resources.
 
    .PARAMETER OutputPath
        Path to write the schema file. If omitted, returns the schema string.
 
    .OUTPUTS
        System.String - JSON Schema
 
    .EXAMPLE
        New-DscSchemaFromFile -ModuleFile './MyResources.psm1'
 
    .EXAMPLE
        New-DscSchemaFromFile -ScriptFile './MyResource.ps1' -OutputPath './schema.json'
    #>

    [CmdletBinding(DefaultParameterSetName = 'ModuleFile')]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ModuleFile')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.psm1$') { throw "ModuleFile must be a .psm1 file: $_" }
            $true
        })]
        [string]$ModuleFile,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ScriptFile')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.ps1$') { throw "ScriptFile must be a .ps1 file: $_" }
            $true
        })]
        [string]$ScriptFile,

        [Parameter()]
        [string]$ResourceName,

        [Parameter()]
        [string]$OutputPath
    )

    $filePath = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') { $ModuleFile } else { $ScriptFile }
    
    $moduleInfo = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') {
        Read-DscResourceModule -ModuleFile $filePath
    } else {
        Read-DscResourceModule -ScriptFile $filePath
    }

    if ($moduleInfo.Resources.Count -eq 0) {
        Write-Warning "No DSC resources found in: $filePath"
        return
    }

    $resources = if ($ResourceName) {
        $moduleInfo.Resources | Where-Object { $_.ClassName -eq $ResourceName }
    }
    else {
        $moduleInfo.Resources
    }

    if (-not $resources -or $resources.Count -eq 0) {
        Write-Warning "DSC resource '$ResourceName' not found in: $filePath"
        return
    }

    $schemaGenerator = New-SchemaGenerator

    foreach ($resource in $resources) {
        $schemaObject = $schemaGenerator.GenerateSchema($resource, $moduleInfo)
        $schema = $schemaObject.ToJsonString($script:JsonOptions)

        if ($OutputPath) {
            Write-FileContent -Path $OutputPath -Content $schema
        }
        else {
            Write-Output $schema
        }
    }
}

function New-DscManifestFromFile {
    <#
    .SYNOPSIS
        Generates a DSC resource manifest from a PowerShell module or script file.
 
    .DESCRIPTION
        Parses a .psm1 or .ps1 file and generates complete Microsoft DSC manifest files
        with embedded JSON schemas.
 
    .PARAMETER ModuleFile
        Path to a PowerShell module file (.psm1).
 
    .PARAMETER ScriptFile
        Path to a PowerShell script file (.ps1).
 
    .PARAMETER ResourceName
        Name of a specific DSC resource to generate manifest for.
        If omitted, generates manifests for all resources found.
 
    .PARAMETER OutputPath
        Path to write the manifest file. If omitted, returns the manifest string.
 
    .PARAMETER Executable
        Executable for manifest operations. Default: pwsh
 
    .PARAMETER ScriptPath
        Script path for manifest operations. Default: ./resource.ps1
 
    .PARAMETER ResourceTypePrefix
        Prefix for resource type name in the manifest (e.g., 'MyCompany/').
        Note: Operation commands use just the class name, not the prefixed type.
 
    .PARAMETER Version
        Resource version. Default: 0.0.1
 
    .PARAMETER Tags
        Array of tags for the manifest.
 
    .OUTPUTS
        System.String - DSC manifest JSON
 
    .EXAMPLE
        New-DscManifestFromFile -ModuleFile './MyResources.psm1'
 
    .EXAMPLE
        New-DscManifestFromFile -ScriptFile './MyResource.ps1' -ResourceTypePrefix 'Test/' -Version '1.0.0'
    #>

    [CmdletBinding(DefaultParameterSetName = 'ModuleFile')]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ModuleFile')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.psm1$') { throw "ModuleFile must be a .psm1 file: $_" }
            $true
        })]
        [string]$ModuleFile,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ScriptFile')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.ps1$') { throw "ScriptFile must be a .ps1 file: $_" }
            $true
        })]
        [string]$ScriptFile,

        [Parameter()]
        [string]$ResourceName,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [string]$Executable = 'pwsh',

        [Parameter()]
        [string]$ScriptPath = './resource.ps1',

        [Parameter()]
        [string]$ResourceTypePrefix = '',

        [Parameter()]
        [string]$Version = '0.0.1',

        [Parameter()]
        [string[]]$Tags = @(),

        [Parameter()]
        [ValidateSet('state', 'stateAndDiff')]
        [string]$SetReturn,

        [Parameter()]
        [ValidateSet('state', 'stateAndDiff')]
        [string]$TestReturn,

        [Parameter()]
        [string]$ScriptArgs
    )

    $filePath = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') { $ModuleFile } else { $ScriptFile }
    
    $moduleInfo = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') {
        Read-DscResourceModule -ModuleFile $filePath
    } else {
        Read-DscResourceModule -ScriptFile $filePath
    }

    if ($moduleInfo.Resources.Count -eq 0) {
        Write-Warning "No DSC resources found in: $filePath"
        return
    }

    $resources = if ($ResourceName) {
        $moduleInfo.Resources | Where-Object { $_.ClassName -eq $ResourceName }
    }
    else {
        $moduleInfo.Resources
    }

    if (-not $resources -or $resources.Count -eq 0) {
        Write-Warning "DSC resource '$ResourceName' not found in: $filePath"
        return
    }

    $options = New-ManifestOptions -Executable $Executable -ScriptPath $ScriptPath `
        -ResourceTypePrefix $ResourceTypePrefix -Version $Version -Tags $Tags `
        -SetReturn $SetReturn -TestReturn $TestReturn -ScriptArgs $ScriptArgs
    
    $manifestGenerator = New-ManifestGenerator

    foreach ($resource in $resources) {
        $manifest = $manifestGenerator.GenerateManifest($resource, $moduleInfo, $options)

        if ($OutputPath) {
            Write-FileContent -Path $OutputPath -Content $manifest
        }
        else {
            Write-Output $manifest
        }
    }
}

function Export-DscManifestFromFile {
    <#
    .SYNOPSIS
        Exports DSC manifests for all resources in a module or script file.
 
    .DESCRIPTION
        Parses a .psm1 or .ps1 file and generates manifest files for all DSC resources,
        writing them to the specified output directory.
 
    .PARAMETER ModuleFile
        Path to a PowerShell module file (.psm1).
 
    .PARAMETER ScriptFile
        Path to a PowerShell script file (.ps1).
 
    .PARAMETER OutputDirectory
        Directory to write the generated manifest files.
 
    .PARAMETER Executable
        Executable for manifest operations. Default: pwsh
 
    .PARAMETER ScriptPath
        Script path for manifest operations. Default: ./resource.ps1
 
    .PARAMETER ResourceTypePrefix
        Prefix for resource type names in the manifest.
        Note: Operation commands use just the class name, not the prefixed type.
 
    .PARAMETER Version
        Resource version. Default: 0.0.1
 
    .PARAMETER Tags
        Array of tags for the manifests.
 
    .PARAMETER PassThru
        If specified, returns the generated file paths.
 
    .OUTPUTS
        System.IO.FileInfo[] - When PassThru is specified
 
    .EXAMPLE
        Export-DscManifestFromFile -ModuleFile './MyResources.psm1' -OutputDirectory './manifests'
 
    .EXAMPLE
        Export-DscManifestFromFile -ScriptFile './MyResource.ps1' -OutputDirectory './out' -PassThru
    #>

    [CmdletBinding(DefaultParameterSetName = 'ModuleFile')]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ModuleFile')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.psm1$') { throw "ModuleFile must be a .psm1 file: $_" }
            $true
        })]
        [string]$ModuleFile,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ScriptFile')]
        [ValidateScript({
            if (-not (Test-Path $_)) { throw "File not found: $_" }
            if ($_ -notmatch '\.ps1$') { throw "ScriptFile must be a .ps1 file: $_" }
            $true
        })]
        [string]$ScriptFile,

        [Parameter(Mandatory)]
        [string]$OutputDirectory,

        [Parameter()]
        [string]$Executable = 'pwsh',

        [Parameter()]
        [string]$ScriptPath = './resource.ps1',

        [Parameter()]
        [string]$ResourceTypePrefix = '',

        [Parameter()]
        [string]$Version = '0.0.1',

        [Parameter()]
        [string[]]$Tags = @(),

        [Parameter()]
        [ValidateSet('state', 'stateAndDiff')]
        [string]$SetReturn,

        [Parameter()]
        [ValidateSet('state', 'stateAndDiff')]
        [string]$TestReturn,

        [Parameter()]
        [string]$ScriptArgs,

        [Parameter()]
        [switch]$PassThru
    )

    # Ensure output directory exists
    if (-not (Test-Path $OutputDirectory)) {
        New-Item -Path $OutputDirectory -ItemType Directory -Force | Out-Null
    }

    $filePath = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') { $ModuleFile } else { $ScriptFile }
    
    $moduleInfo = if ($PSCmdlet.ParameterSetName -eq 'ModuleFile') {
        Read-DscResourceModule -ModuleFile $filePath
    } else {
        Read-DscResourceModule -ScriptFile $filePath
    }

    if ($moduleInfo.Resources.Count -eq 0) {
        Write-Warning "No DSC resources found in: $filePath"
        return
    }

    Write-Host "Found $($moduleInfo.Resources.Count) DSC resource(s) in: $filePath" -ForegroundColor Cyan

    $options = New-ManifestOptions -Executable $Executable -ScriptPath $ScriptPath `
        -ResourceTypePrefix $ResourceTypePrefix -Version $Version -Tags $Tags `
        -SetReturn $SetReturn -TestReturn $TestReturn -ScriptArgs $ScriptArgs
    
    $manifestGenerator = New-ManifestGenerator
    $generatedFiles = @()

    foreach ($resource in $moduleInfo.Resources) {
        $fileName = "$($resource.ClassName).dsc.resource.json"
        $outputFile = Join-Path $OutputDirectory $fileName
        
        $manifest = $manifestGenerator.GenerateManifest($resource, $moduleInfo, $options)
        [System.IO.File]::WriteAllText($outputFile, $manifest)
        
        Write-Host " Generated: $outputFile" -ForegroundColor Green
        
        if ($PassThru) {
            $generatedFiles += Get-Item $outputFile
        }
    }

    if ($PassThru) {
        return $generatedFiles
    }
}

#endregion

#region Public Functions - Type-based (use .NET types)

function New-DscSchemaFromClass {
    <#
    .SYNOPSIS
        Generates JSON Schema from a .NET type.
 
    .DESCRIPTION
        Takes a .NET type with DSC attributes and generates a JSON Schema
        representing its structure.
 
    .PARAMETER Type
        The .NET type to generate schema for.
 
    .PARAMETER OutputPath
        Path to write the schema file. If omitted, returns the schema string.
 
    .OUTPUTS
        System.String - JSON Schema
 
    .EXAMPLE
        New-DscSchemaFromClass -Type ([MyModule.NewFileResource])
 
    .EXAMPLE
        [MyModule.NewFileResource] | New-DscSchemaFromClass -OutputPath './schema.json'
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [Alias('InputObject')]
        [Type]$Type,

        [Parameter()]
        [string]$OutputPath
    )

    process {
        $generator = [DSCSchemaGenerator.Services.DscSchemaGenerator]::new()
        $schemaObject = $generator.GenerateSchema($Type)
        $schema = $schemaObject.ToJsonString($script:JsonOptions)

        if ($OutputPath) {
            Write-FileContent -Path $OutputPath -Content $schema
        }
        else {
            Write-Output $schema
        }
    }
}

function New-DscManifestFromClass {
    <#
    .SYNOPSIS
        Generates a DSC manifest from a .NET type.
 
    .DESCRIPTION
        Takes a .NET type with DSC attributes and generates a complete
        Microsoft DSC manifest with embedded JSON schema.
 
    .PARAMETER Type
        The .NET type to generate manifest for.
 
    .PARAMETER OutputPath
        Path to write the manifest file. If omitted, returns the manifest string.
 
    .PARAMETER Executable
        Executable for manifest operations. Default: pwsh
 
    .PARAMETER ScriptPath
        Script path for manifest operations. Default: ./resource.ps1
 
    .PARAMETER SetReturn
        The return type for the Set operation. Valid values: 'state', 'stateAndDiff'.
        When not specified, no return property is added to the manifest.
 
    .PARAMETER TestReturn
        The return type for the Test operation. Valid values: 'state', 'stateAndDiff'.
        When not specified, no return property is added to the manifest.
 
    .PARAMETER ScriptArgs
        Optional additional arguments to pass to the resource script.
        These are appended before the -operation argument.
        Example: "-resourcetype myresource" or "-verbose"
 
    .OUTPUTS
        System.String - DSC manifest JSON
 
    .EXAMPLE
        New-DscManifestFromClass -Type ([MyModule.NewFileResource])
 
    .EXAMPLE
        New-DscManifestFromClass -Type ([MyModule.NewFileResource]) -OutputPath './manifest.json'
 
    .EXAMPLE
        New-DscManifestFromClass -Type ([MyModule.NewFileResource]) -SetReturn 'state' -TestReturn 'state'
 
    .EXAMPLE
        New-DscManifestFromClass -Type ([MyModule.NewFileResource]) -ScriptArgs '-resourcetype myresource'
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [Alias('InputObject')]
        [Type]$Type,

        [Parameter()]
        [string]$OutputPath,

        [Parameter()]
        [string]$Executable = 'pwsh',

        [Parameter()]
        [string]$ScriptPath = './resource.ps1',

        [Parameter()]
        [ValidateSet('state', 'stateAndDiff')]
        [string]$SetReturn,

        [Parameter()]
        [ValidateSet('state', 'stateAndDiff')]
        [string]$TestReturn,

        [Parameter()]
        [string]$ScriptArgs
    )

    process {
        $generator = [DSCSchemaGenerator.Services.DscManifestGenerator]@{
            Executable = $Executable
            ScriptPath = $ScriptPath
            SetReturn  = $SetReturn
            TestReturn = $TestReturn
            ScriptArgs = $ScriptArgs
        }

        $manifest = $generator.GenerateManifest($Type)

        if ($OutputPath) {
            Write-FileContent -Path $OutputPath -Content $manifest
        }
        else {
            Write-Output $manifest
        }
    }
}

#endregion

#region Module Exports

Export-ModuleMember -Function @(
    # File-based functions (parse .psm1 directly)
    'Read-DscResourceModule'
    'New-DscSchemaFromFile'
    'New-DscManifestFromFile'
    'Export-DscManifestFromFile'
    # Type-based functions (use .NET types)
    'New-DscSchemaFromClass'
    'New-DscManifestFromClass'
)

#endregion