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 |