OpenAPIParsing.ps1
<#
.SYNOPSIS This is an internal-only argument completer cmdlet, to be used in the ArgumentCompleter attribute, which returns a list of AnyChain endpoints or valid HTTP methods for the Endpoint or or Method parameters. It also returns sample query parameter strings or a sample structure for the HTTP body structure. This cmdlet is intended for internal use and is not designed to be used by clients of this module. It is exported so the command shell to access it. .NOTES For information on PowerShell argument completion, see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_argument_completion #> function GetEndpointArguments { param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) # Only attempt if the OpenAPI specification file has been provided. if ( $AnyChainProfile -and (Test-Path -Path $ExecutionContext.InvokeCommand.ExpandString($AnyChainProfile.AnyChainOpenAPISpecPath)) ) { # For which parameter do we want to provide argument suggestions? switch ( $parameterName ) { 'Endpoint' { # Perform calculation of tab completed values here. $filterByMethod = $fakeBoundParameters.Keys -contains 'Method' # If the Method parameter was specified, filter for endpoints with the specified HTTP method. # Then return a sorted list of the HTTP methods, enclosed in quotes if braces are present. $AnyChainOpenAPISpecification.paths.GetEnumerator() | Where-Object { -not $filterByMethod -or $_.Value.Keys -contains $fakeBoundParameters['Method'] } | Where-Object { $_.Key.StartsWith($wordToComplete) -or $_.Key.TrimStart('/').StartsWith($wordToComplete) } | Select-Object -ExpandProperty Key | Sort-Object | ForEach-Object { $_ -match '{|}' ? "`"$_`"" : $_ } } 'Method' { # Return list of HTTP verbs, restricted to those supported by the specified endpoint, if specified. $methods = @( 'get','post','patch','delete' ); if ( $AnyChainOpenAPISpecification -and $fakeBoundParameters.Keys -contains 'Endpoint' ) { $methods = $AnyChainOpenAPISpecification.paths[ $fakeBoundParameters['Endpoint'] ].Keys | Where-Object { $_ -in $methods } } $methods | Where-Object { $_.StartsWith($wordToComplete) } | Sort-Object -Unique } 'QueryParams' { # Return a sample query parameter string for this endpoint/method if ( $fakeBoundParameters.Keys -contains 'Endpoint' -and $fakeBoundParameters.Keys -contains 'Method' -and $fakeBoundParameters['Endpoint'] -in $AnyChainOpenAPISpecification.paths.Keys ) { # Get all the endpoint and method parameters for this endpoint. # Then format the query parameters as an HTTP query string. @() + (Get-EndpointQueryParams -Endpoint $fakeBoundParameters['Endpoint'] -Method $fakeBoundParameters['Method']) | Sort-Object | Join-String -FormatString '{0}=null' -Separator '&' -OutputPrefix '"' -OutputSuffix '"' } } 'Body' { # Return a list of body parameters for this endpoint and method if ( $fakeBoundParameters.Keys -contains 'Endpoint' -and $fakeBoundParameters.Keys -contains 'Method' -and $fakeBoundParameters['Endpoint'] -in $AnyChainOpenAPISpecification.paths.Keys ) { # Get one or more request body samples as objects, then convert them to JSON and/or hashtable syntax strings. $() + (Get-EndpointBody -Endpoint $fakeBoundParameters['Endpoint'] -Method $fakeBoundParameters['Method']) | ForEach-Object ` { # Generate a JSON string of the sample body. $jsonBody = $_ | ConvertTo-Json -Compress -Depth 50 -EnumsAsStrings # Return the sample body using PowerShell hashtable-syntax string.. # Note that this conversion is heuristic and not guaranteed to be correct. It is "tuned" to work with the current spec's sample data. $jsonBody.Replace('":','"=').Replace('\"user\"= ','\"user\": ').Replace('null','$null').Replace(',',';').Replace('[','@(').Replace(']',')').Replace('\"','`"') -replace '(?<!"){','@{' } } } 'ExpandProperty' { if ( $fakeBoundParameters.Keys -contains 'Endpoint' -and $fakeBoundParameters.Keys -contains 'Method' -and $fakeBoundParameters['Endpoint'] -in $AnyChainOpenAPISpecification.paths.Keys ) { # Get one or more request responses as objects, then convert them to JSON and/or hashtable syntax strings. $() + (Get-EndpointResponse -Endpoint $fakeBoundParameters['Endpoint'] -Method $fakeBoundParameters['Method']) | Select-Object -ExpandProperty Keys | Sort-Object } } } } } <# Returns the structure of the Body for the specified endpoint and method. #> function Get-EndpointBody( $Endpoint, $Method ) { $AnyChainOpenAPISpecification.paths.$Endpoint.$Method | Select-Object -ExpandProperty requestBody -ErrorAction SilentlyContinue | Get-ApiSpecRequestBody -Method $Method } <# Returns the structure of the responses for the specified endpoint and method. #> function Get-EndpointResponse( $Endpoint, $Method ) { try { $AnyChainOpenAPISpecification.paths.$Endpoint.$Method | Select-Object -ExpandProperty responses -ErrorAction SilentlyContinue | ForEach-Object { $_.GetEnumerator() | # Where-Object Name -like '2*' | ForEach-Object { # Get the schema and parse it $def = DerefOASItem -RefItem $_.Value ($def.content.GetEnumerator() | Where-Object Key -like 'application/*').Value.schema | Get-ApiSpecBodyItems -Method $Method } } } catch { # return nothing } } <# Returns the query parameters for the specified endpoint and method. #> function Get-EndpointQueryParams( $Endpoint, $Method ) { # Get all the endpoint and method parameters for this endpoint $endpointSpecParams = $AnyChainOpenAPISpecification.paths.$($Endpoint).Item('parameters') $methodSpecParams = $AnyChainOpenAPISpecification.paths.$($Endpoint).$($Method).Item('parameters') # Filter for only the query parameters $methodParams = if ( $methodSpecParams ) { $methodSpecParams.GetEnumerator() | Get-ReferencedApiSpecItem | Where-Object -Property 'in' -EQ -Value 'query' } $endpointParams = if ( $endpointSpecParams) { $endpointSpecParams.GetEnumerator() | Get-ReferencedApiSpecItem | Where-Object -Property 'in' -EQ -Value 'query' } @() + ($endpointParams | Select-Object -ExpandProperty name) + ($methodParams | Select-Object -ExpandProperty name) } <# This function parses the definition of an endpoint body to provide one or more hashtable strings for the command line auto-completion of the Body parameter. #> filter Get-ApiSpecRequestBody ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $Item, [Parameter(Mandatory=$true)] $Method ) { # Ensure the item is non-null if ( $Item ) { # If the item is a dictionary/hashtable with a $ref key, dereference it. $body = $Item if ( $body -is [System.Collections.IDictionary] -and $body.Containskey( '$ref' ) ) { # This is a reference. Find the referenced element. $body = DerefOASItem -RefItem $body } # Get the schema $schema = ($body.content.GetEnumerator() | Where-Object Key -like 'application/*').Value.schema # Parse the body and return one or more sample body parameter strings. Get-ApiSpecBodyItems -Item $schema -Method $Method } } # Returns the dereferenced item (i.e., the item specified by the $ref property), if present, or the original item. function DerefOASItem( $RefItem ) { # If the item is a dictionary/hashtable with a $ref key, dereference it. if ( $RefItem -is [System.Collections.IDictionary] -and $RefItem.Containskey( '$ref' ) ) { # This is a reference. Find the referenced element. $deref = $AnyChainOpenAPISpecification foreach ( $elem in ($RefItem.'$ref'.TrimStart('#/') -split '/') ) { $deref = $deref.$elem } DerefOASItem -RefItem $deref } else { $RefItem } } <# Recursively parse the tree structure of the endpoint body definition. #> function Get-ApiSpecBodyItems { Param ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $Item, [Parameter(Mandatory=$true)] $Method ) Process { # Ensure the item is non-null Write-Debug -Message ("Input: " + (ConvertTo-Json $Item -Compress -Depth 10)) if ( $Item ) { # If the item is a dictionary/hashtable with a $ref key, dereference it. $Item = DerefOASItem -RefItem $Item # Parse the next level in the body specification. $properties = switch ( $Item ) { # Parsing a schema element, which seems to have no specific indicator # other than it is parsed as a dictionary element. { $_ -is [System.Collections.DictionaryEntry] } { Get-ApiSpecBodyItems -Item $_.Value -Method $Method break } # allOf { $_.ContainsKey('allOf') } { $result = @{} $_.allOf | Get-ApiSpecBodyItems -Method $Method | ForEach-Object -Process { $_.GetEnumerator() | ForEach-Object { $result[$_.Key] = $_.Value } } $result break } # oneOf # This is key to getting more than one set of options. For each item returned (via the pipeline), # the recursive caller will generate a separate string, which will propagate all the way up to # the first caller, resulting in multiple auto-completion lines/options being presented. { $_.ContainsKey('oneOf') } { $_.oneOf | Get-ApiSpecBodyItems -Method $Method break } # Object { $_.ContainsKey('type') -and ($_.type -eq 'object' -or $_.ContainsKey('properties')) } { $result = @{} $_.properties.GetEnumerator() | ForEach-Object -Process ` { # Find the dereferenced property. If this is a GET and the property is not `readonly`, # add the property to the return object (i.e., hashtable). $propValue = DerefOASItem -RefItem $_.Value if ( $Method -eq 'get' -or -not ($propValue -is [System.Collections.Hashtable] -and $propValue.ContainsKey('readOnly') -and $propValue.readOnly -eq 'true' ) ) { # Parse the specification for this nested property. $value = $propValue | Get-ApiSpecBodyItems -Method $Method # Add this property to the hashtable for this object. if ( $propValue -is [System.Collections.Hashtable] -and $propValue.ContainsKey('type') -and $propValue.type -eq 'array' ) { # An array of one item is converted (via the pipeline) to a single item. Convert it back to an array. $result.Add( $_.Key, @($value) ) } else { $result.Add( $_.Key, $value ) } } } $result break } # Array { $_.ContainsKey('type') -and $_.type -eq 'array' } { # Recursively retrieve and return the array of body items. # IMPORTANT: Note that an array of one item will be converted to the one item by the pipeline. $_.items | Get-ApiSpecBodyItems -Method $Method break } # Value # If possible, provide a sample value for this element. { $_.ContainsKey('type') -and $_.type -in 'number','integer','boolean','string','date-time' } { # Don't fail due to an exception parsing values; just fallback to null. try { # The value format is based on the data type. $itemVal = $_ switch ( $itemVal.type ) { # Boolean type 'boolean' { $itemVal.ContainsKey('example') ? $itemVal.example : 'true' break } # String types can have a format property. Use that to format the sample value. 'string' { if ( $itemVal.ContainsKey('enum') ) { $itemVal.enum[0] break } # If the format property is not defined, return the example provided, or an empty string. if ( -not $itemVal.ContainsKey('format') ) { $itemVal.ContainsKey('example') ? $itemVal.example : '' break } # Use the example switch ( $itemVal.format ) { 'date-time' { $itemVal.ContainsKey('example') ? [System.DateTimeOffset]::Parse($itemVal.example).ToString('yyyy-MM-ddTHH:mm:ssZ') : (Get-Date -AsUTC -Format 'yyyy-MM-ddTHH:mm:ssZ') } 'date' { $itemVal.ContainsKey('example') ? [System.DateTimeOffset]::Parse($itemVal.example).ToString('yyyy-MM-dd') : (Get-Date -AsUTC -Format 'yyyy-MM-dd') } 'uuid' { [Guid]::Empty } default { $itemVal.ContainsKey('example') ? $itemVal.example.ToString() : '' } } break } # Numeric type { $_ -in 'number','integer' } { $itemVal.ContainsKey('example') ? $itemVal.example : 0 break } default { "" } } } catch { $null } } } # Return the hashtable of properties Write-Debug -Message ("Output: " + (ConvertTo-Json $properties -Compress -Depth 10) ) $properties } } } # Dererence an OpenAPI item (e.g., parameter) by returning the item it references (if $ref) or the item itself. filter Get-ReferencedApiSpecItem ( [Parameter(Mandatory=$true,ValueFromPipeline=$true)] $Item ) { # Ensure the item is non-null if ( $Item ) { # If the item is a dictionary/hashtable with a $ref key, dereference it. if ( $Item -is [System.Collections.IDictionary] -and $Item.Containskey('$ref') ) { # This is a reference. Find and return the referenced element. DerefOASItem -RefItem $Item } else { # Return the original item. $Item } } } |