functions/ConvertFrom-ARSwagger.ps1
function ConvertFrom-ARSwagger { <# .SYNOPSIS Parse a swagger file and generate commands from it. .DESCRIPTION Parse a swagger file and generate commands from it. Only supports the JSON format of swagger file. .PARAMETER Path Path to the swagger file(s) to process. .PARAMETER TransformPath Path to a folder containing psd1 transform files. These can be used to override or add to individual entries from the swagger file. For example, you can add help URI, fix missing descriptions, add parameter help or attributes... .PARAMETER RestCommand Name of the command executing the respective REST queries. All autogenerated commands will call this command to execute. .PARAMETER ConvertToHashtableCommand In order to make it easier to include a version of `ConvertTo-Hashtable` you can rename the used function name by using this parameter. Defaults to `ConvertTo-Hashtable`. .PARAMETER ModulePrefix A prefix to add to all commands generated from this command. .PARAMETER PathPrefix Swagger files may include the same first uri segments in all endpoints. While this could be just passed through, you can also remove them using this parameter. It is then assumed, that the command used in the RestCommand is aware of this and adds it again to the request. Example: All endpoints in the swagger-file start with "/api/" "/api/users", "/api/machines", "/api/software", ... In that case, it could make sense to remove the "/api/" part from all commands and just include it in the invokation command. .PARAMETER ServiceName Adds the servicename to the commands generated. When exported, they will be hardcoded to execute as that service. This simplifies the configuration of the output, but prevents using multiple connections to different instances or under different privileges at the same time. .EXAMPLE PS C:\> Get-ChildItem .\swaggerfiles | ConvertFrom-ARSwagger -Transformpath .\transform -RestCommand Invoke-ARRestRequest -ModulePrefix Mg -PathPrefix '/api/' Picks up all items in the subfolder "swaggerfiles" and converts it to PowerShell command objects. Applies all transforms in the subfolder transform. Uses the "Invoke-ARRestRequest" command for all rest requests. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidateScript('PSFramework.Validate.FSPath.File', ErrorString = 'PSFramework.Validate.FSPath.File')] [Alias('FullName')] [string] $Path, [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] [string] $TransformPath, [Parameter(Mandatory = $true)] [string] $RestCommand, [string] $ConvertToHashtableCommand = 'ConvertTo-Hashtable', [string] $ModulePrefix, [string] $PathPrefix, [string] $ServiceName ) begin { #region Functions function Copy-ParameterConfig { [CmdletBinding()] param ( [Hashtable] $Config, $Parameter ) if ($Config.Help) { $Parameter.Help = $Config.Help } if ($Config.Name) { $Parameter.Name = $Config.Name } if ($Config.Alias) { $Parameter.Alias = $Config.Alias } if ($Config.Weight) { $Parameter.Weight = $Config.Weight } if ($Config.ParameterType) { $Parameter.ParameterType = $Config.ParameterType } if ($Config.ContainsKey('ValueFromPipeline')) { $Parameter.ValueFromPipeline = $Config.ValueFromPipeline } if ($Config.ParameterSet) { $Parameter.ParameterSet = $Config.ParameterSet } } function New-Parameter { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Name, [string] $Help, [string] $ParameterType, [AllowEmptyString()] [AllowNull()] [string] $ParameterFormat, [bool] $Mandatory, [ParameterType] $Type ) $parameter = [CommandParameter]::new( $Name, $Help, $ParameterType, $Mandatory, $Type ) if ($parameter.ParameterType -eq "integer") { $parameter.ParameterType = $ParameterFormat } $parameter } function Resolve-ParameterReference { [CmdletBinding()] param ( [string] $Ref, $SwaggerObject ) # "#/components/parameters/top" $segments = $Ref | Set-String -OldValue '^#/' | Split-String -Separator '/' $paramValue = $SwaggerObject foreach ($segment in $segments) { $paramValue = $paramValue.$segment } $paramValue } function Read-Parameters { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param ( [Command] $CommandObject, $Parameters, $SwaggerObject, [PSFramework.Message.MessageLevel] $LogLevel, [string] $ParameterSet ) foreach ($parameter in $Parameters) { if ($parameter.'$ref') { $parameter = Resolve-ParameterReference -Ref $parameter.'$ref' -SwaggerObject $SwaggerObject if (-not $parameter) { Write-PSFMessage -Level Warning -Message " Unable to resolve referenced parameter $($parameter.'$ref')" continue } } if ($LogLevel -le [PSFramework.Message.MessageLevel]::Verbose) { # This is on hot path. Checking if we should write the message in a cheap way. Write-PSFMessage " Processing Parameter: $($parameter.Name) ($($parameter.in))" } switch ($parameter.in) { #region Body body { foreach ($property in $parameter.schema.properties.PSObject.Properties) { if ($ParameterSet -and $CommandObject.Parameters[$property.Value.title]) { $CommandObject.Parameters[$property.Value.title].ParameterSet += @($ParameterSet) continue } $parameterParam = @{ Name = $property.Value.title Help = $property.Value.description ParameterType = $property.Value.type ParameterFormat = $property.Value.format Mandatory = $parameter.schema.required -contains $property.Value.title Type = 'Body' } $CommandObject.Parameters[$property.Value.title] = New-Parameter @parameterParam if ($ParameterSet) { $commandObject.Parameters[$property.Value.title].ParameterSet = @($ParameterSet) } } } #endregion Body #region Path path { if ($ParameterSet -and $CommandObject.Parameters[($parameter.name -replace '\s')]) { $CommandObject.Parameters[($parameter.name -replace '\s')].ParameterSet += @($ParameterSet) continue } $parameterParam = @{ Name = $parameter.Name -replace '\s' Help = $parameter.Description ParameterType = 'string' ParameterFormat = $parameter.format Mandatory = $parameter.required -as [bool] Type = 'Path' } $CommandObject.Parameters[($parameter.name -replace '\s')] = New-Parameter @parameterParam if ($ParameterSet) { $CommandObject.Parameters[($parameter.name -replace '\s')].ParameterSet = @($ParameterSet) } } #endregion Path #region Query query { if ($CommandObject.Parameters[$parameter.name]) { $CommandObject.Parameters[$parameter.name].ParameterSet += @($parameterSetName) continue } $parameterType = $parameter.type if (-not $parameterType -and $parameter.schema.type) { $parameterType = $parameter.schema.type if ($parameter.schema.type -eq "array" -and $parameter.schema.items.type) { $parameterType = '{0}[]' -f $parameter.schema.items.type } } $parameterParam = @{ Name = $parameter.Name Help = $parameter.Description ParameterType = $parameterType ParameterFormat = $parameter.format Mandatory = $parameter.required -as [bool] Type = 'Query' } $CommandObject.Parameters[$parameter.name] = New-Parameter @parameterParam if ($ParameterSet) { $CommandObject.Parameters[$parameter.name].ParameterSet = @($ParameterSet) } } #endregion Query #region Header header { if ($commandObject.Parameters[$parameter.name]) { $commandObject.Parameters[$parameter.name].ParameterSet += @($parameterSetName) continue } $parameterType = $parameter.type if (-not $parameterType -and $parameter.schema.type) { $parameterType = $parameter.schema.type if ($parameter.schema.type -eq "array" -and $parameter.schema.items.type) { $parameterType = '{0}[]' -f $parameter.schema.items.type } } $parameterParam = @{ Name = $parameter.Name Help = $parameter.Description ParameterType = $parameterType ParameterFormat = $parameter.format Mandatory = $parameter.required -as [bool] Type = 'header' } $commandObject.Parameters[$parameter.name] = New-Parameter @parameterParam $commandObject.Parameters[$parameter.name].ParameterSet = @($parameterSetName) } #endregion Header } } } function Set-ParameterOverrides { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param ( [hashtable] $Overrides, [Command] $CommandObject, [string] $CommandKey ) foreach ($parameterName in $Overrides.globalParameters.Keys) { if (-not $CommandObject.Parameters[$parameterName]) { continue } Copy-ParameterConfig -Config $Overrides.globalParameters[$parameterName] -Parameter $CommandObject.Parameters[$parameterName] } foreach ($partialPath in $Overrides.scopedParameters.Keys) { if ($CommandObject.EndpointUrl -notlike $partialPath) { continue } foreach ($parameterPair in $Overrides.scopedParameters.$($partialPath).GetEnumerator()) { if (-not $CommandObject.Parameters[$parameterPair.Name]) { continue } Copy-ParameterConfig -Parameter $CommandObject.Parameters[$parameterPair.Name] -Config $parameterPair.Value } } foreach ($parameterName in $Overrides.$CommandKey.Parameters.Keys) { if (-not $CommandObject.Parameters[$parameterName]) { Write-PSFMessage -Level Warning -Message "Invalid override parameter: $parameterName - unable to find parameter on $($CommandObject.Name)" -Target $commandObject continue } Copy-ParameterConfig -Config $Overrides.$CommandKey.Parameters[$parameterName] -Parameter $CommandObject.Parameters[$parameterName] } } function Set-CommandOverrides { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param ( [hashtable] $Overrides, [Command] $CommandObject, [string] $CommandKey ) $commandOverrides = $Overrides.$CommandKey # Apply Overrides foreach ($property in $CommandObject.PSObject.Properties) { if ($property.Name -eq 'Parameters') { continue } if ($property.Name -eq 'ParameterSets') { foreach ($key in $commandOverrides.ParameterSets.Keys) { $CommandObject.ParameterSets[$key] = $commandOverrides.ParameterSets.$key } continue } $propertyOverride = $commandOverrides.($property.Name) if ($propertyOverride) { $property.Value = $propertyOverride } } } #endregion Functions $commands = @{ } $overrides = @{ } if ($TransformPath) { foreach ($file in Get-ChildItem -Path $TransformPath -Filter *.psd1) { $data = Import-PSFPowerShellDataFile -Path $file.FullName foreach ($key in $data.Keys) { $overrides[$key] = $data.$key } } } $verbs = @{ get = "Get" put = "New" post = "Set" patch = "Set" delete = "Remove" head = "Invoke" } [PSFramework.Message.MessageLevel]$logLevel = Get-PSFConfigValue -FullName AutoRest.Logging.Level -Fallback "Warning" } process { #region Process Swagger file foreach ($file in Resolve-PSFPath -Path $Path) { $data = Get-Content -Path $file | ConvertFrom-Json foreach ($endpoint in $data.paths.PSObject.Properties | Sort-Object { $_.Name.Length }, Name) { $endpointPath = ($endpoint.Name -replace "^$PathPrefix" -replace '/{[\w\s\d+-]+}$').Trim("/") $effectiveEndpointPath = ($endpoint.Name -replace "^$PathPrefix" -replace '\s' ).Trim("/") foreach ($method in $endpoint.Value.PSObject.Properties) { $commandKey = $endpointPath, $method.Name -join ":" if ($logLevel -le [PSFramework.Message.MessageLevel]::Verbose) { Write-PSFMessage "Processing Command: $($commandKey)" } #region Case: Existing Command if ($commands[$commandKey]) { $commandObject = $commands[$commandKey] $parameterSetName = $method.Value.operationId $commandObject.ParameterSets[$parameterSetName] = $method.Value.description Read-Parameters -CommandObject $commandObject -Parameters $method.Value.parameters -SwaggerObject $data -LogLevel $logLevel -ParameterSet $parameterSetName } #endregion Case: Existing Command #region Case: New Command else { $commandNouns = foreach ($element in $endpointPath -split "/") { if ($element -like "{*}") { continue } [cultureinfo]::CurrentUICulture.TextInfo.ToTitleCase($element) -replace 's$' -replace '\$' } $commandObject = [Command]@{ Name = "$($verbs[$method.Name])-$($ModulePrefix)$($commandNouns -join '')" Synopsis = $method.Value.summary Description = $method.Value.description Method = $method.Name EndpointUrl = $effectiveEndpointPath RestCommand = $RestCommand ParameterSets = @{ 'default' = $method.Value.description } ConvertToHashtableCommand = $ConvertToHashtableCommand } if ($ServiceName) { $commandObject.ServiceName = $ServiceName } $commands[$commandKey] = $commandObject foreach ($property in $commands[$commandKey].PSObject.Properties) { if ($property.Name -eq 'Parameters') { continue } if ($overrides.$commandKey.$($property.Name)) { $commandObject.$($property.Name) = $overrides.$commandKey.$($property.Name) } } # Parameters Read-Parameters -CommandObject $commandObject -Parameters $method.Value.parameters -SwaggerObject $data -LogLevel $logLevel } #endregion Case: New Command if ($logLevel -le [PSFramework.Message.MessageLevel]::Verbose) { Write-PSFMessage -Message "Finished processing $($endpointPath) : $($method.Name) --> $($commandObject.Name)" -Target $commandObject -Data @{ Overrides = $overrides CommandObject = $commandObject } -Tag done } } } } #endregion Process Swagger file } end { foreach ($pair in $commands.GetEnumerator()) { Set-ParameterOverrides -Overrides $overrides -CommandObject $pair.Value -CommandKey $pair.Key Set-CommandOverrides -Overrides $overrides -CommandObject $pair.Value -CommandKey $pair.Key } $commands.Values } } |