PSDocs.Dsc.psm1

#
# PSDocs DSC extensions module
#

Set-StrictMode -Version latest;

class DscMofDocument {

    [System.Collections.Generic.Dictionary[String, PSObject[]]]$ResourceType

    [System.Collections.Generic.Dictionary[String, PSObject]]$ResourceId

    [String]$Path

    [String]$InstanceName

    DscMofDocument() {
        $this.ResourceId = @{ };
        $this.ResourceType = @{ };
    }
}

#
# Localization
#

$LocalizedData = data {
    
}

Import-LocalizedData -BindingVariable LocalizedData -FileName 'PSDocs.Dsc.Resources.psd1' -ErrorAction SilentlyContinue;

#
# Public functions
#

# .ExternalHelp PSDocs.Dsc-Help.xml
function Invoke-DscNodeDocument {

    [CmdletBinding()]
    param (
        # The name of the document
        [Parameter(Mandatory = $False)]
        [String]$DocumentName,
        
        # A script or path to the script to run
        [Parameter(Mandatory = $False)]
        [String]$Script,

        [Parameter(Mandatory = $False)]
        [String[]]$InstanceName,

        # The path to the .mof files
        [Parameter(Mandatory = $False)]
        [String]$Path = $PWD,

        # The path to output documentation
        [Parameter(Mandatory = $False)]
        [String]$OutputPath = $PWD,

        [Parameter(Mandatory = $False)]
        [PSDocs.Configuration.MarkdownEncoding]$Encoding = [PSDocs.Configuration.MarkdownEncoding]::Default
    )

    begin {
        Write-Verbose -Message "[Invoke-DscNodeDocument]::BEGIN";
    }

    process {
        # Build the documentation
        BuildDocumentation @PSBoundParameters;
    }

    end {
        Write-Verbose -Message "[Invoke-DscNodeDocument]::END";
    }
}

# .ExternalHelp PSDocs.Dsc-Help.xml
function Get-DscMofDocument {

    [CmdletBinding()]
    [OutputType([DscMofDocument])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {
        # Import and return the .mof as an object containing resource instances
        ImportMofDocument -Path $Path -Verbose:$VerbosePreference;
    }
}

#
# Helper functions
#

function BuildDocumentation {

    [CmdletBinding()]
    [OutputType([void])]
    param (
        [Parameter(Mandatory = $False)]
        [String]$DocumentName,

        [Parameter(Mandatory = $False)]
        [String]$Script,

        [Parameter(Mandatory = $False)]
        [String[]]$InstanceName,

        # The path to the .mof file
        [Parameter(Mandatory = $False)]
        [String]$Path = $PWD,

        # The output path to store documentaion
        [Parameter(Mandatory = $False)]
        [String]$OutputPath = $PWD,

        [Parameter(Mandatory = $False)]
        [PSDocs.Configuration.MarkdownEncoding]$Encoding
    )

    process {

        if (!(Test-Path -Path $Path)) {
            throw (New-Object -TypeName System.IO.DirectoryNotFoundException);
        }

        $Path = Resolve-Path -Path $Path;

        $referenceConfig = New-Object -TypeName System.Collections.Generic.List[PSObject];

        try {
            # Look for .mof file within the path
            $referenceConfigFilePath = FindMofDocument -Path $Path -InstanceName $InstanceName;

            if ($Null -eq $referenceConfigFilePath -or $referenceConfigFilePath.Length -le 0) {
                return;
            }

            # Extract a reference configuration for each .mof file
            foreach ($file in $referenceConfigFilePath) {
                $referenceConfig.Add((ImportMofDocument -Path $file -Verbose:$VerbosePreference));
            }
        }
        catch {
            Write-Error -Message ($LocalizedData.ImportMofFailed -f $Path, $_.Exception.Message) -Exception $_.Exception -ErrorAction Stop;
        }

        foreach ($r in $referenceConfig) {
            Write-Verbose -Message "[Doc][Mof] -- Using: $($r.Path)";

            $invokeParams = @{
                InstanceName = $r.InstanceName
                InputObject = $r
                OutputPath = $OutputPath
            }

            if ($PSBoundParameters.ContainsKey('Encoding')) {
                $invokeParams['Encoding'] = $Encoding;
            }

            if ($PSBoundParameters.ContainsKey('Script')) {
                $invokeParams['Path'] = $Script;

                Invoke-PSDocument @invokeParams -Verbose:$VerbosePreference;
            }
            else {
                $documentFn = Get-ChildItem -Path "Function:\$DocumentName" -ErrorAction Ignore;

                if ($Null -eq $documentFn) {
                    Write-Error "Failed for find document";

                    continue;
                }

                # Generate a document for the configuration
                [ScriptBlock]::Create([String]::Concat($DocumentName,' @invokeParams -Verbose:$VerbosePreference;')).Invoke();
            }
        }

        # Write-Verbose -Message "[Doc][$dokOperation] -- Update TOC: $($buildResult.FullName)";

        # Update TOC
        # UpdateToc -OutputPath $OutputPath -Verbose:$VerbosePreference;
    }
}

# Builds a configuration graph from a .mof file
function ImportMofDocument {

    [CmdletBinding()]
    [OutputType([DscMofDocument])]
    param (
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        Write-Verbose -Message "[Doc][Mof][Import]::BEGIN";

        # Parse a .mof file and extract object instances
        $instances = ParseMofDocument -Path $Path -Verbose:$VerbosePreference;

        # Extract the instance name from the .mof file name
        $Path -match '\\((?<name>[A-Z0-9_]{3,})(\.meta){0,}\.mof)$' | Out-Null;
        $instanceName = $Matches.name;

        # Build a configuration object
        $result = New-Object -TypeName DscMofDocument -Property @{
            InstanceName = $instanceName
            Path = $path
        };

        # Process each instance and inde by id and type
        foreach ($instance in $instances) {

            $resourceId = $instance.ResourceId;
            $resourceType = $Null;

            if (![String]::IsNullOrEmpty($resourceId)) {

                # Extract resource type from ResourceId
                if ($resourceId -match '^(\s{0,}\[(?<type>[A-Z0-9_:]*)\][A-Z0-9_:\]\[]*)$') {
                    $resourceType = $Matches.type;
                }

                Write-Verbose -Message "[Doc][Mof][Import] -- Adding resource id: $resourceId";
                
                # Add the instance indexed by ResourceId
                $result.ResourceId[$resourceId] = $instance;
                
                if ($Null -ne $resourceType) {
                    if (!$result.ResourceType.ContainsKey($resourceType)) {
                        Write-Verbose -Message "[Doc][Mof][Import] -- Adding resource type: $resourceType";

                        $result.ResourceType.Add($resourceType, @());
                    }
                    
                    # Add the instance indexed by ResourceType
                    $result.ResourceType[$resourceType] += $instance;
                }
            }
        }

        # Emit the mof graph object to the pipeline
        $result;

        Write-Verbose -Message "[Doc][Mof][Import]::END";
    }
}

# Parses a .mof file into object insances
function ParseMofDocument {
    [CmdletBinding()]
    param (
        # The path to the .mof file
        [Parameter(Mandatory = $True)]
        [String]$Path
    )

    process {

        Write-Verbose -Message "[Doc][Mof][Import] -- Parsing: $Path";

        # Split the .mof into instances
        $instances = ((Get-Content $Path -Raw) -split "\n(?=instance of)" -match 'instance of ([A-Z_]*) as');

        # This variable will store configuration items
        $result = New-Object -TypeName System.Collections.Generic.List[PSObject];

        # Process each instance
        foreach ($instance in $instances) {

            # This variable will store properties for a single configuration item
            $props = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,Object]'([System.StringComparer]::OrdinalIgnoreCase);

            # Extract out properties from mof instance block
            $instance -match '\n\{(\r|\n)(?<props>(.|\n)+)\};' | Out-Null;

            # Cleanup new line, line feeds and space padding
            $inner = ($Matches.props -replace '(\r|\n){1,}\s{1,}', "`n") -replace "\n\r", "`n" -split ";\n";

            # Process each property for the configuration item
            $inner | ForEach-Object -Process {

                # Break out key value pairs
                $prop = ($_ -replace '\r|\n', '') -Split '\s{0,}=\s{0,}',2;

                # Ensure that a key value pair was found
                if ($prop.Length -eq 2) {
                    
                    # Cleanup value by removing quotes, line feeds and escaped slashes
                    $value = ($prop[1] -replace '^(\")|(\"(\;\r){0,})$', '') -replace '\\\\', '\';

                    # Look for array values
                    if ($value -match '^(\{(?<array>.*)\})$') {

                        $valueArray = $Matches.array;

                        # Look for string array for type convertion
                        if ($valueArray -match '(^\"|\"$)') {

                            # Force value to be a string array, and cleanup quotes
                            $value = [String[]]@(($valueArray -split '","' -replace '(^\"|\"$)', ''));
                        } else {

                            # Convert value to object array
                            $value = $valueArray -split ',';
                        }
                    }

                    # Add key value pair to dictionary
                    $props[$prop[0]] = $value;
                }
            }

            # Add object based on properties to result
            $result.Add((New-Object -TypeName PSObject -Property $props));
        }

        # Emit result to the pipeline
        $result;

        Write-Verbose -Message "[Doc][Mof][Import] -- Found instances: $($result.Count)";
    }
}

# Finds .mof file in a specified path
function FindMofDocument {

    [CmdletBinding()]
    [OutputType([String])]
    param (
        # The directory path to search for .mof files within
        [Parameter(Mandatory = $True)]
        [String]$Path,

        # An optional InstanceName filter to filter .mof files returned
        [Parameter(Mandatory = $False)]
        [String[]]$InstanceName
    )

    process {
        Write-Verbose -Message "[Doc][Mof] -- Scanning for .mof files in: $Path";

        # Search for mof files
        $items = Get-ChildItem -Path $Path -Filter *.mof -File;

        foreach ($item in $items) {
            if ($Null -eq $InstanceName -or $InstanceName -contains $item.BaseName) {
                # Emit the full name of a mof file to the pipeline when it matches the criteria
                $item.FullName;
            }
        }
    }
}

#
# Export module
#

Export-ModuleMember -Function @(
    'Invoke-DscNodeDocument'
    'Get-DscMofDocument'
)

# EOM