Public/Get-IBSchema.ps1

function Get-IBSchema {
    [CmdletBinding()]
    param(
        [Alias('type')]
        [string]$ObjectType,
        [switch]$Raw,
        [switch]$LaunchHTML,
        [string[]]$Fields,
        [string[]]$Operations,
        [switch]$NoFields,
        [string[]]$Functions,
        [switch]$NoFunctions,
        [switch]$Detailed,

        [ValidateScript({Test-ValidProfile $_ -ThrowOnFail})]
        [string]$ProfileName,
        [ValidateScript({Test-NonEmptyString $_ -ThrowOnFail})]
        [Alias('host')]
        [string]$WAPIHost,
        [ValidateScript({Test-VersionString $_ -ThrowOnFail})]
        [Alias('version')]
        [string]$WAPIVersion,
        [PSCredential]$Credential,
        [switch]$SkipCertificateCheck
    )

    # grab the variables we'll be using for our REST calls
    try { $opts = Initialize-CallVars @PSBoundParameters } catch { $PsCmdlet.ThrowTerminatingError($_) }
    $WAPIHost = $opts.WAPIHost
    $WAPIVersion = $opts.WAPIVersion

    # add a schema cache for this host if it doesn't exist
    if (-not $script:Schemas.$WAPIHost) {
        $script:Schemas.$WAPIHost = @{ ReadFields = @{} }
    }
    $sCache = $script:Schemas.$WAPIHost

    # make sure we can actually query schema stuff for this WAPIHost
    if (-not $sCache.HighestVersion) {
        $sCache.HighestVersion = (HighestVer @opts)
        Write-Debug "Set highest version: $($sCache.HighestVersion)"
    }
    if ([Version]$sCache.HighestVersion -lt [Version]'1.7.5') {
        $PSCmdlet.ThrowTerminatingError([Management.Automation.ErrorRecord]::new(
            "NIOS WAPI $($sCache.HighestVersion) doesn't support schema queries",
            $null, [Management.Automation.ErrorCategory]::InvalidOperation, $null
        ))
    }

    # cache some base schema stuff that we'll potentially need later
    if (-not $sCache.SupportedVersions -or -not $sCache[$WAPIVersion]) {
        $schema = Invoke-IBWAPI -Query '?_schema' @opts

        # set supported versions
        $sCache.SupportedVersions = $schema.supported_versions | Sort-Object @{E={[Version]$_}}
        Write-Debug "Set supported versions: $($sCache.SupportedVersions -join ', ')"

        # set supported objects for this version
        $sCache[$WAPIVersion] = $schema.supported_objects | Sort-Object
        Write-Debug "Set supported objects for $($WAPIVersion): $($sCache[$WAPIVersion] -join ', ')"
    }

    # The 'request' object is a weird outlier that only accepts POST requests against it
    # and I haven't been able to figure out how to query its schema using POST. So for
    # now, just warn and exit if we've been asked to query it.
    if ($ObjectType -eq 'request') {
        Write-Warning "The 'request' object is not currently supported for schema queries"
        return
    }

    if (![String]::IsNullOrWhiteSpace($ObjectType)) {
        # We want to support wildcard searches and partial matching on object types.
        Write-Debug "ObjectType: $ObjectType"
        $objMatches = $sCache[$WAPIVersion] | ForEach-Object { if ($_ -like $ObjectType) { $_ } }
        Write-Debug "Matches: $($objMatches.Count)"
        if ($objMatches.count -gt 1) {
            # multiple matches
            $message = "Multiple object matches found for $($ObjectType)"
            if ($Raw) {
                $PSCmdlet.ThrowTerminatingError([Management.Automation.ErrorRecord]::new(
                    $message,$null, [Management.Automation.ErrorCategory]::LimitsExceeded, $null
                ))
            }
            Write-Output "$($message):"
            $objMatches | ForEach-Object { Write-Output $_ }
            return
        }
        elseif ($objMatches.count -eq 0 ) {
            Write-Debug "Retrying matches with implied wildcards"
            # retry matching with implied wildcards
            $objMatches = $sCache[$WAPIVersion] | ForEach-Object { if ($_ -like "*$ObjectType*") { $_ } }
            if ($objMatches.count -gt 1) {
                # multiple matches
                $message = "Multiple object matches found for $($ObjectType)"
                if ($Raw) {
                    $PSCmdlet.ThrowTerminatingError([Management.Automation.ErrorRecord]::new(
                        $message,$null, [Management.Automation.ErrorCategory]::LimitsExceeded, $null
                    ))
                }
                Write-Output "$($message):"
                $objMatches | ForEach-Object { Write-Output $_ }
                return
            }
            elseif ($objMatches.count -eq 0) {
                # no matches, even with wildcards
                $message = "No matches found for $($ObjectType)"
                if ($Raw) {
                    $PSCmdlet.ThrowTerminatingError([Management.Automation.ErrorRecord]::new(
                        $message,$null, [Management.Automation.ErrorCategory]::ObjectNotFound, $null
                    ))
                }
                else { Write-Warning $message }
                return
            } else {
                $ObjectType = $objMatches
            }
        }
        else {
            # only one match
            $ObjectType = $objMatches
        }
    }

    # As of WAPI 2.6 (NIOS 8.1), schema queries get a lot more helpful with the addition of
    # _schema_version, _schema_searchable, and _get_doc. The odd thing is that those fields
    # are even available if you query old WAPI versions. But if you're *actually* on an
    # old WAPI version, they generate an error.
    #
    # We want to give people as much information as possible. So instead of conditionally
    # using the additional schema options if the requested WAPI version supports it, we want
    # to always do it as long as the latest *supported* WAPI version supports them.
    $query = '{0}?_schema=1' -f $ObjectType
    if ([Version]$sCache.HighestVersion -ge [Version]'2.6') {
        $query += "&_schema_version=2&_schema_searchable=1&_get_doc=1"
    }

    $schema = Invoke-IBWAPI -Query $query @opts

    # check for the switches that will prevent additional output
    if ($Raw -or $LaunchHTML) {
        # return the schema object directly, if asked
        if ($Raw) { Write-Output $schema }
        # launch a browser window to the object's full docs
        if ($LaunchHTML) {
            $docBase = $script:WAPIDocTemplate -f $WAPIHost
            if ([String]::IsNullOrWhiteSpace($ObjectType)) {
                Start-Process "$($docBase)index.html"
            } else {
                Start-Process "$($docBase)objects/$($ObjectType.Replace(':','.')).html"
            }
        }
        return
    }

    function BlankLine() { Write-Output '' }
    function PrettifySupports([string]$supports) {
        # The 'supports' property of a schema Field is a lower cases string
        # containing one or more of r,w,u,d,s for the supported operations of
        # that field. Most, but not all, are in a standard order. There are
        # instances of things like 'wu' vs 'uw'. We want to standardize the
        # order (RWUSD), uppercase the letters, and insert spaces for the operations
        # not included in the list.
        $ret = ''
        'R','W','U','D','S' | ForEach-Object {
            if ($supports -like "*$_*") {
                $ret += $_
            } else {
                $ret += ' '
            }
        }
        $ret
        # Basic string concatentation obviously isn't the most efficient thing to
        # do here. But we can optimize later if it becomes a problem.
    }
    function PrettifySupportsDetail([string]$supports) {
        # The 'supports' property of a schema Field is a lower cases string
        # containing one or more of r,w,u,s,d for the supported operations of
        # that field. Most, but not all, are in a standard order. There are
        # instances of things like 'wu' vs 'uw'. We want to spell out the operations
        # for the detailed view.
        $ret = @()
        if ($supports -like '*r*') { $ret += 'Read' }
        if ($supports -like '*w*') { $ret += 'Write' }
        if ($supports -like '*u*') { $ret += 'Update' }
        if ($supports -like '*d*') { $ret += 'Delete' }
        if ($supports -like '*s*') { $ret += 'Search' }
        ($ret -join ', ')
        # Basic string concatentation obviously isn't the most efficient thing to
        # do here. But we can optimize later if it becomes a problem.
    }

    function PrettifyType($field) {

        if ($field.enum_values) {
            $type = "{ $($field.enum_values -join ' | ') }"
            if ($field.is_array) { $type = "$type[]" }
        } else {
            if ($field.is_array) {
                $type = ($field.type | ForEach-Object { "$_[]" }) -join ' | '
            } else {
                $type = $field.type -join '|'
            }
        }

        $type
    }

    if (!$schema.type) {
        # base schema object
        BlankLine
        Write-Output "Requested Version: $($schema.requested_version)"
        BlankLine
        Write-Output "Supported Versions:"
        BlankLine
        Write-Output ($schema.supported_versions | Sort-Object @{E={[Version]$_}} | Format-Columns -prop {$_} -col 4)
        BlankLine
        Write-Output "Supported Objects:"
        BlankLine
        Write-Output ($schema.supported_objects | Format-Columns -prop {$_})
        BlankLine
    }
    else {
        # display the top level object info
        $typeStr = "$($schema.type) (WAPI $($schema.version))"
        BlankLine
        Write-Output 'OBJECT'
        Write-Output ($typeStr | Split-Str -Indent 4)
        if ($schema.restrictions) {
            BlankLine
            Write-Output 'RESTRICTIONS'
            Write-Output ("$($schema.restrictions -join ', ')" | Split-Str -Indent 4)
        }
        if ($schema.cloud_additional_restrictions) {
            BlankLine
            Write-Output 'CLOUD RESTRICTIONS'
            Write-Output ("$($schema.cloud_additional_restrictions -join ', ')" | Split-Str -Indent 4)
        }

        # With _schema_version=2, functions are returned in the normal
        # list of fields. But we want to split those out and display them differently.
        $fieldList = @($schema.fields | Where-Object { $_.wapi_primitive -ne 'funccall' })
        $funcList  = @($schema.fields | Where-Object { $_.wapi_primitive -eq 'funccall' })

        # filter the fields if specified
        if ($Fields.count -gt 0) {
            $fieldList = @($fieldList | Where-Object {
                $name = $_.name
                ($Fields | ForEach-Object { $name -like $_ }) -contains $true
            })
        }
        # filter fields that don't include at least one specified Operation unless no operations were specified
        if ($Operations.count -gt 0) {
            $fieldList = @($fieldList | Where-Object {
                $supports = $_.supports
                ($Operations | ForEach-Object { $supports -like "*$_*"}) -contains $true
            })
        }
        # filter the functions if specified
        if ($Functions.count -gt 0) {
            $funcList = @($funcList | Where-Object {
                $name = $_.name
                ($Functions | ForEach-Object { $name -like $_ }) -contains $true
            })
        }

        if ($fieldList.count -gt 0 -and !$NoFields) {

            if ($Detailed) {
                # Display the detailed view

                BlankLine
                Write-Output 'FIELDS'

                # loop through fields alphabetically
                $fieldList | Sort-Object name | ForEach-Object {

                    Write-Output ("$($_.name) <$(PrettifyType $_)>" | Split-Str -Indent 4)

                    if ($_.doc) {
                        Write-Output ($_.doc | Split-Str -Indent 8)
                    }
                    BlankLine

                    Write-Output ("Supports: $(PrettifySupportsDetail $_.supports)" | Split-Str -Indent 8)

                    if ($_.overridden_by) {
                        Write-Output ("Overridden By: $($_.overridden_by)" | Split-Str -Indent 8)
                    }
                    if ($_.standard_field) {
                        Write-Output ("This field is part of the base object." | Split-Str -Indent 8)
                    }
                    if ($_.supports_inline_funccall) {
                        Write-Output ("This field supports inline function calls. See full docs for more detail." | Split-Str -Indent 8)
                    }
                    if ($_.searchable_by) {
                        BlankLine
                        Write-Output ("This field is available for search via:" | Split-Str -Indent 8)
                        if ($_.searchable_by -like '*=*') { Write-Output ("'=' (exact equality)" | Split-Str -Indent 12) }
                        if ($_.searchable_by -like '*!*') { Write-Output ("'!=' (negative equality)" | Split-Str -Indent 12) }
                        if ($_.searchable_by -like '*:*') { Write-Output ("':=' (case insensitive search)" | Split-Str -Indent 12) }
                        if ($_.searchable_by -like '*~*') { Write-Output ("'~=' (regular expression)" | Split-Str -Indent 12) }
                        if ($_.searchable_by -like '*<*') { Write-Output ("'<=' (less than or equal to)" | Split-Str -Indent 12) }
                        if ($_.searchable_by -like '*>*') { Write-Output ("'>=' (greater than or equal to)" | Split-Str -Indent 12) }
                    }

                    # At this point, the only other thing to potentially deal with is if this field is
                    # a struct. If so, there will be a sub-schema object with it's own set of fields. But
                    # each of those fields might also be a struct with even more sub-schemas, potentially going
                    # 3+ levels deep. Even the HTML docs don't try to cram all that into a field's description.
                    # They stick with links to the struct details.

                    # Unfortunately, the schema queries don't support querying structs directly. So in order to
                    # fake making something like that work, we would need to basically cache struct definitions
                    # (per WAPI version) as they're queried. Maybe have some way to pre-cache all the structs for
                    # a particular version by whipping through the supported object types?

                    # In any case, it's a non-trivial task for another day. And I don't want it to delay the schema
                    # querying release.

                    BlankLine
                }

            } else {
                # Display the simple view

                # get the length of the longest field name so we can make sure not to truncate that column
                $nameMax = [Math]::Max(($fieldList.name | Sort-Object -desc @{E={$_.length}} | Select-Object -first 1).length + 1, 6)
                # get the length of the longest type name (including potential array brackets) so we can
                # make sure not to truncate that column
                $typeMax = [Math]::Max(($fieldList.type | Sort-Object -desc @{E={$_.length}} | Select-Object -first 1).length + 3, 5)

                $format = "{0,-$nameMax}{1,-$typeMax}{2,-9}{3,-5}{4,-6}"
                BlankLine
                Write-Output ($format -f 'FIELD','TYPE','SUPPORTS','BASE','SEARCH')
                Write-Output ($format -f '-----','----','--------','----','------')

                # loop through fields alphabetically
                $fieldList | Sort-Object @{E='name';Desc=$false} | ForEach-Object {

                    # set the Base column value
                    $base = ''
                    if ($_.standard_field) { $base = 'X' }

                    # put brackets on array types
                    if ($_.is_array) {
                        for ($i=0; $i -lt $_.type.count; $i++) {
                            $_.type[$i] = "$($_.type[$i])[]"
                        }
                    }

                    # there should always be at least one type, so write that with the rest of
                    # the table values
                    Write-Output ($format -f $_.name,$_.type[0],(PrettifySupports $_.supports),$base,$_.searchable_by)

                    # write additional types on their own line
                    if ($_.type.count -gt 1) {
                        for ($i=1; $i -lt $_.type.count; $i++) {
                            Write-Output "$(''.PadRight($nameMax))$($_.type[$i])"
                        }
                    }
                }
            } # end simple field view
        } # end fields

        if ($funcList.count -gt 0 -and !$NoFunctions) {

            BlankLine
            Write-Output "FUNCTIONS"

            if ($Detailed) {

                $funcList | ForEach-Object {
                    BlankLine
                    Write-Output ' ----------------------------------------------------------'
                    Write-Output ($_.name | Split-Str -Indent 4)
                    Write-Output ' ----------------------------------------------------------'
                    if ($_.doc) {
                        Write-Output ($_.doc | Split-Str -Indent 8)
                    }
                    if ($_.schema.input_fields.count -gt 0) {
                        BlankLine
                        Write-Output ("INPUTS" | Split-Str -Indent 4)
                        foreach ($field in $_.schema.input_fields) {
                            BlankLine
                            Write-Output ("$($field.name) <$(PrettifyType $field)>" | Split-Str -Indent 8)
                            Write-Output ($field.doc | Split-Str -Indent 12)
                        }
                    }
                    if ($_.schema.output_fields.count -gt 0) {
                        BlankLine
                        Write-Output ("OUTPUTS" | Split-Str -Indent 4)
                        foreach ($field in $_.schema.output_fields) {
                            BlankLine
                            Write-Output ("$($field.name) <$(PrettifyType $field)>" | Split-Str -Indent 8)
                            Write-Output ($field.doc | Split-Str -Indent 12)
                        }
                    }

                }

            } else {

                $funcList | ForEach-Object {
                    $funcListtr = "$($_.name)($($_.schema.input_fields.name -join ', '))"
                    if ($_.schema.output_fields.count -gt 0) {
                        $funcListtr += " => $($_.schema.output_fields.name -join ', ')"
                    }
                    Write-Output ($funcListtr | Split-Str -Indent 4)
                }

            } # end simple function view
        } # end functions

        BlankLine
    }
}