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 |