synedgy.universal.helper.psm1

using namespace System.Collections
#Region './Classes/00.Attributes/APIEndpoint.ps1' -1

class APIEndpoint : System.Attribute
{
    [bool]$IsEndpoint = $true # Indicates that this is an API endpoint attribute
    [bool]$AutoSerialization = $true # Whether the endpoint should automatically serialize the output to JSON
    [string]$Name
    [string]$version = 'v1' # Version of the API endpoint
    [string]$Path # aka the URL path of the endpoint
    [string]$Description # Description of the endpoint
    [ValidateSet('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD')]
    [string[]]$Method # HTTP method (GET, POST, PUT, DELETE, etc.)
    [bool]$Authentication = $false # Whether the endpoint requires authentication
    [string[]]$Role # Role required to access the endpoint
    [string]$Tag # Tag for categorizing the endpoint
    [int]$Timeout# Timeout for the endpoint in seconds
    [string]$Environment # Environment where the endpoint is executed (e.g. the PowerShell Universal environment)
    [string]$ContentType = 'application/json; charset=utf-8' # Content type of the response
    [ValidateSet('Information', 'Warning', 'Error', 'Verbose', 'Debug')] # TODO: Create the scriptlbock to add the -$loglevelAction Continue
    [string[]]$LogLevel = @('Information') # Log level for the endpoint
    [scriptblock]$Parameters # Override of parameters for the endpoint to splat to the command

    APIEndpoint ()
    {
        # Default constructor for the attribute
    }
}
#EndRegion './Classes/00.Attributes/APIEndpoint.ps1' 26
#Region './Public/Get-ModuleApiEndpoint.ps1' -1

function Get-ModuleApiEndpoint
{
    <#
    .SYNOPSIS
        Retrieves the API endpoint for the current module.

    .DESCRIPTION
        This function retrieves the API endpoint metadata for a specified service by getting the public functions of the module.

    .EXAMPLE
        Get-ModuleApiEndpoint
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.IDictionary])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'No parameters needed')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Output type is a dictionary')]
    param
    (
        [Parameter()]
        [Microsoft.PowerShell.Commands.ModuleSpecification]
        $Module,

        [Parameter()]
        [ValidateSet('Information', 'Debug', 'Verbose')]
        [string[]]
        $LogLevel,

        [Parameter()]
        [string]
        $Environment,

        [Parameter()]
        [switch]
        $Authentication,

        [Parameter()]
        [string]
        $ApiPrefix
    )

    # TODO: Load module in Thread and process it there (once done the thread is closed and DLL handles freed)
    $apiModule = Get-Module $Module -ErrorAction Stop
    Write-Verbose -Message ('Module: {0} Version: {1}' -f $apiModule.Name, $apiModule.Version)
    $endpoints = $apiModule.ExportedFunctions.Values.Where{
        $_.ScriptBlock.Attributes.Where{
            $_.TypeId.ToString() -eq 'APIEndpoint' -and $_.IsEndpoint -eq $true
        }
    }

    Write-Verbose -Message ('Discovered API functions are: {0}' -f ($endpoints.Name -join ', '))
    foreach ($functionEndpoint in $endpoints)
    {
        # You can have more than one endpoint for a single function
        # by repeating the APIEndpoint attribute
        # This is useful for versioning or different methods, and overriding parameters signature (say between v1 and v2)
        $ApiEndpoints = $functionEndpoint.ScriptBlock.Attributes.Where{
            $_.TypeId.ToString() -eq 'APIEndpoint' -and $_.IsEndpoint -eq $true
        }

        foreach ($ApiEndpoint in $ApiEndpoints)
        {
            $urlPath = $ApiEndpoint.Path
            $version = $ApiEndpoint.version
            $Name = $ApiEndpoint.Name

            if ([string]::IsNullOrEmpty($urlPath))
            {
                $urlPath = $functionEndpoint.Noun.ToLower()
            }

            if ($PSBoundParameters.ContainsKey('ApiPrefix'))
            {
                $url = ('{0}/{1}' -f $PSBoundParameters['ApiPrefix'], $urlPath.TrimStart('/').ToLower()) -replace '//', '/'
            }
            else
            {
                $url = (('/{0}/{1}/{2}' -f $endpoints.Name, $ApiEndpoint.version, $urlPath.TrimStart('/')).ToLower()) -replace '//', '/'
            }

            Write-Information -MessageData "Processing endpoint: $url for function: $($functionEndpoint.Name)"
            if ([string]::IsNullOrEmpty($Name))
            {
                $Name = '{0}{1}{2}' -f $functionEndpoint.verb.ToLower(), $functionEndpoint.Noun,$version
            }

            $description = $ApiEndpoint.Description
            if ([string]::IsNullOrEmpty($description))
            {
                $description = (Get-Help -Name $functionEndpoint.Name -ErrorAction SilentlyContinue).Description.Text
            }

            $method = $ApiEndpoint.Method
            if ([string]::IsNullOrEmpty($method))
            {
                $method = Get-HttpMethodFromPSVerb -Verb $functionEndpoint.Verb
            }

            $apiEndpointData = @{
                # Mandatory fields for the endpoint
                url          = $url
                Description  = $description
                Method       = $method
                FunctionInfo = $functionEndpoint
            }

            if ($PSBoundParameters.ContainsKey('LogLevel'))
            {
                $apiEndpointData['LogLevel'] = $PSBoundParameters['LogLevel']
            }
            elseif ($ApiEndpoint.LogLevel -and $ApiEndpoint.LogLevel.Count -gt 0)
            {
                $apiEndpointData['LogLevel'] = $ApiEndpoint.LogLevel
            }

            # Optional fields for the endpoint
            if ($PSBoundParameters.ContainsKey('Environment'))
            {
                $apiEndpointData['Environment'] = $PSBoundParameters['Environment']
            }
            elseif (-not [string]::IsNullOrEmpty($ApiEndpoint.Environment))
            {
                $apiEndpointData['Environment'] = $ApiEndpoint.Environment
            }

            if ($PSBoundParameters.ContainsKey('Authentication'))
            {
                $apiEndpointData['Authentication'] = $PSBoundParameters['Authentication']
            }
            elseif ($ApiEndpoint.Authentication)
            {
                $apiEndpointData['Authentication'] = $ApiEndpoint.Authentication
            }

            if ($PSBoundParameters.ContainsKey('Role'))
            {
                $apiEndpointData['Role'] = $PSBoundParameters['Role']
            }
            elseif ($ApiEndpoint.Role -and $ApiEndpoint.Role.Count -gt 0)
            {
                $apiEndpointData['Role'] = $ApiEndpoint.Role
            }

            if ($ApiEndpoint.Tag -gt 0)
            {
                $apiEndpointData['Tag'] = $ApiEndpoint.Tag
            }

            if ($ApiEndpoint.Timeout -and $ApiEndpoint.Timeout -gt 0)
            {
                $apiEndpointData['Timeout'] = $ApiEndpoint.Timeout
            }

            if (-not [string]::IsNullOrEmpty($ApiEndpoint.ContentType))
            {
                $apiEndpointData['ContentType'] = $ApiEndpoint.ContentType
            }

            if ($ApiEndpoint.Parameters -and $ApiEndpoint.Parameters -is [scriptblock])
            {
                $apiEndpointData['Parameters'] = $ApiEndpoint.Parameters
            }

            $apiEndpointData
        }
    }
}
#EndRegion './Public/Get-ModuleApiEndpoint.ps1' 167
#Region './Public/Get-ModuleApiEndpointScriptblock.ps1' -1


#using namespace System.Collections
function Get-ModuleApiEndpointScriptblock
{
    <#
        .SYNOPSIS
        Returns a scriptblock that can be used as an endpoint for a PSU API.

        .DESCRIPTION
        This function returns a scriptblock that can be used as an endpoint for the module API.
        The scriptblock will return the module's API endpoints.

        .EXAMPLE
        $scriptBlock = Get-ModuleApiEndpointScriptblock
        Invoke-Command -ScriptBlock $scriptBlock

        .OUTPUTS
        [PSCustomObject[]] - An array of custom objects representing the module's API endpoints.
    #>

    [CmdletBinding()]
    [OutputType([ScriptBlock])]
    param
    (
        [Parameter(Mandatory = $true)]
        [IDictionary]
        $ModuleApiEndpoint
    )

    #region Force Endpoint to have pascalCase parameters
    $paramBlock = $ModuleApiEndpoint.FunctionInfo.ScriptBlock.ast.Body.ParamBlock
    $paramBlockStr = $paramBlock.Extent.Text
    # change the Extent to camelCase the Extent name
    $paramBlock.Parameters | ForEach-Object {
        # Update $_.Name to be camelCase
        $parameter = $_

        # If the parameter name is not in camelCase, convert it
        $FirstChar = $parameter.Name.Extent.text.Substring(1, 1) # first letter after the $
        $RestOfName = $parameter.Name.Extent.text.Substring(2) # rest of the name after the first letter
        $newParamName = '${0}{1}' -f $FirstChar.ToLowerInvariant(),$RestOfName
        $startOffSet = $parameter.Name.Extent.StartOffset - $paramBlock.Extent.StartOffset
        $paramBlockStr = $paramBlockStr.Remove($startOffSet,$newParamName.Length).Insert($startOffSet,$newParamName)

        #TODO: Change switch parameter to bool
    }
    #endregion

    if ($PSBoundParameters.ContainsKey('Environment'))
    {
        $paramBlockStr = $paramBlockStr -replace '\$Environment', '$PSBoundParameters["Environment"]'
    }

    [string] $InformationAction, $VerboseAction, $DebugAction = ''

    if ($ModuleApiEndpoint.LogLevel -contains 'Information')
    {
        $InformationAction = '$InformationPreference = ''Continue'';'
    }

    if ($ModuleApiEndpoint.LogLevel -contains 'Verbose')
    {
        $VerboseAction = '$VerbosePreference = ''Continue'';'
    }

    if ($ModuleApiEndpoint.LogLevel -contains 'Debug')
    {
        $DebugAction = '$DebugPreference = ''Continue'';'
    }

    $testingStream = '
    Write-Host "this is a Write-Host test message"
    Write-Information "this is a Write-Information test message"
    Write-Debug -Message "This is a Write-Debug test message"
    Write-Verbose -Verbose -Message "This is a Write-Verbose test message"
'

    $loggingConfig = @(
        $InformationAction,
        $VerboseAction,
        $DebugAction
        # $testingStream
        #TODO: raise a bug with PowerShell Universal (only Information/Write-Host works)
    ) -join ''


    $functionCall = '{0}{1} {2} @PSBoundParameters{1}' -f $loggingConfig,"`r`n",$ModuleApiEndpoint.FunctionInfo.Name
    # $functionCall = '$result = {0} @PSBoundParameters' -f $ModuleApiEndpoint.FunctionInfo.Name
    # JSON automatic serialization
    # $jsonConvert = '$result | ConvertTo-Json -Depth 10'
    $scriptBlockStr = ' {0}{1}{1} {2}{1} {3}' -f $paramBlockStr,"`r`n", $functionCall,$jsonConvert
    Write-Information -MessageData ('Creating scriptblock for module API endpoint: {0}. {1}' -f $ModuleApiEndpoint.url, $scriptBlockStr)
    # TODO: Implement Generic HTTP codes

    # Create a scriptblock from the string
    $scriptBlock = [scriptblock]::Create($scriptBlockStr)
    return $scriptBlock
}
#EndRegion './Public/Get-ModuleApiEndpointScriptblock.ps1' 97
#Region './Public/Import-PSUEndpoint.ps1' -1

function Import-PSUEndpoint
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([Object])]
    param
    (
        [Parameter()]
        [Microsoft.PowerShell.Commands.ModuleSpecification]
        $Module,

        [Parameter()]
        [string]
        $Environment,

        [Parameter()]
        [string]
        $ApiPrefix,

        [Parameter()]
        [switch]
        # Force authentication on the endpoint regardless of its configuration.
        $Authentication,

        [Parameter()]
        [ValidateSet('Information','Debug', 'Verbose')]
        [string[]]
        $LogLevel = 'Information'
    )

    $moduleApiEndpointParams = @{}

    if ($PSBoundParameters.ContainsKey('Module'))
    {
        $moduleApiEndpointParams['Module'] = $Module
    }

    if ($PSBoundParameters.ContainsKey('Environment'))
    {
        $moduleApiEndpointParams['Environment'] = $Environment
    }

    if ($PSBoundParameters.ContainsKey('ApiPrefix'))
    {
        $moduleApiEndpointParams['ApiPrefix'] = $ApiPrefix
    }

    if ($PSBoundParameters.ContainsKey('Authentication'))
    {
        $moduleApiEndpointParams['Authentication'] = $Authentication
    }

    if ($PSBoundParameters.ContainsKey('LogLevel'))
    {
        $moduleApiEndpointParams['LogLevel'] = $LogLevel
    }

    $endpoints = Get-ModuleApiEndpoint @moduleApiEndpointParams
    $endpoints | ForEach-Object {
        # Create a new endpoint for each function that has the APIEndpoint attribute
        $psuEndpointParams = @{} + $_
        $psuCommand = Get-Command -Name 'New-PSUEndpoint'
        $psuEndpointParams.Keys.Where{
            $_ -notin $psuCommand.Parameters.Keys
        } | ForEach-Object {
            $psuEndpointParams.Remove($_)
        }

        $moduleEndpointScriptblockParameters = @{
            ModuleApiEndpoint = $_
        }

        $endpointScriptBlock = Get-ModuleApiEndpointScriptblock @moduleEndpointScriptblockParameters

        if ($PSCmdlet.ShouldProcess(('Creating PSUEndpoint with {0}' -f ($psuEndpointParams | ConvertTo-Json -depth 6 -Compress))))
        {
            New-PSUEndpoint @psuEndpointParams -Endpoint $endpointScriptBlock
        }
    }
}
#EndRegion './Public/Import-PSUEndpoint.ps1' 80
#Region './suffix.ps1' -1

#region TypeAccelerator export for classes

# If classes/types are specified here, they won't be fully qualified (careful with conflicts)
$typesToExportAsIs = @(
    'APIEndpoint'
)

# The type accelerators created will be ModuleName.ClassName (to avoid conflicts with other modules until you use 'using moduleName'
$typesToExportWithNamespace = @(

)

# inspired from https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.5

# Always clobber an existing type accelerator, but
# warn if a type accelerator with the same name exists.
function Get-CurrentModule
{
    <#
      .SYNOPSIS
      This is a Private function to always be able to retrieve the module info even outside
      of a function (i.e. PSM1 during module loading)

      .DESCRIPTION
      This function is only meant to be used from the psm1, hence not exported.

      .EXAMPLE
      $null = Get-CurrentModule

      #>

    [OutputType([System.Management.Automation.PSModuleInfo])]
    param
    ()

    # Get the current module
    $MyInvocation.MyCommand.ScriptBlock.Module
}

# Get the internal TypeAccelerators class to use its static methods.
$typeAcceleratorsClass = [psobject].Assembly.GetType(
    'System.Management.Automation.TypeAccelerators'
)
$moduleName = (Get-CurrentModule).Name
$existingTypeAccelerators = $typeAcceleratorsClass::Get

foreach ($typeToExport in  @($typesToExportWithNamespace + $typesToExportAsIs))
{
    if ($typeToExport -in $typesToExportAsIs)
    {
        $fullTypeToExport = $TypeToExport
    }
    else
    {
        $fullTypeToExport = '{0}.{1}' -f $moduleName,$TypeToExport
    }

    $type = $TypeToExport -as [System.Type]
    if (-not $type)
    {
        $Message = @(
            "Unable to register type accelerator '$fullTypeToExport' for '$typeToExport'"
            "Type '$typeToExport' not found."
        ) -join ' - '

        throw [System.Management.Automation.ErrorRecord]::new(
                [System.InvalidOperationException]::new($Message),
                'TypeAcceleratorTypeNotFound',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $fullTypeToExport
            )
    }
    else
    {
        if ($fullTypeToExport -in $existingTypeAccelerators.Keys)
        {
            $Message = @(
                "Overriding type accelerator '$($fullTypeToExport)' with '$($Type.FullName)'"
                'Accelerator already exists.'
            ) -join ' - '

            Write-Warning -Message $Message
        }
        else
        {
            Write-Verbose -Message "Added type accelerator '$($fullTypeToExport)' for '$($Type.FullName)'."
        }

        $null = $TypeAcceleratorsClass::Add($fullTypeToExport, $Type)
    }
}

# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    foreach ($TypeName in $typesToExportWithNamespace)
    {
        $fullTypeToExport = '{0}.{1}' -f $moduleName,$TypeName
        $null = $TypeAcceleratorsClass::Remove($fullTypeToExport)
    }
}.GetNewClosure()

#endregion
#EndRegion './suffix.ps1' 102