AnyChain.psm1

<#
.SYNOPSIS
This module can be used to use the Forte AnyChain API from a PowerShell command line or script.
 
.NOTES
To install the `powershell-yaml` module, which can be used by this module to parse a YAML-encoded OpenAPI definition of the AnyChain API (though JSON is preferred), execute the following command:
> Install-Module -Name powershell-yaml
 
.DESCRIPTION
This module provides several functions for accessing the Forte AnyChain API.
 
The basic functionality provided is:
 
* Persist profile settings for specific Forte environments, relieving the user of the need to specify the configuration values with each endpoint request.
 
* Manage the acquisition of tokens for accessing the API.
NOTE: This module does not monitor for expired tokens; it leaves token management to the user.
 
* Constructs HTTP requests by converting strings or hashtables into the HTML body, query, and path parameters.
 
* Use the OpenAPI Specification file (JSON or YAML format) to provide context-sensitive parameter prompts when accessing AnyChain endpoints and programmatic access to the AnyChain API specification.
#>



Set-StrictMode -Version latest
$ErrorActionPreference = 'Stop'


<#
.SYNOPSIS
Creates a new AnyChain profile.
 
WARNING: Be aware the PowerShell has a global variable named $profile; do not assign a value to that variable name.
 
.DESCRIPTION
This cmdlet creates a new AnyChain profile. Note that it does not activate it or persist the profile. The profile can be persisted using the Export-AnyChainProfile or activated via Use-AnyChainProfile.
 
The profile values for an AnyChain profile can be specified explicitly, implicitly by passing an existing profile to this cmdlet via the pipeline, or a combination of both, which allows copying a profile and overriding some of the settings.
 
.EXAMPLE
New-AnyChainProfile -Description "<config-description>" -BaseUrl "https://<app-domain>.anychain.forte.io" -ApplicationID <appid> -AnyChainOpenAPISpecPath "$env:USERPROFILE\\.forte-restish\\spec\\anychain_api.json" | Export-AnyChainProfile -Path <profile-path>.clixml
 
This example create a new profile using explicit parameters, then exports the profile to a file via Export-AnyChainProfile.
 
.EXAMPLE
Get-AnyChainAppProfile | New-AnyChainProfile -Description 'Test description' | Export-AnyChainProfile -Path '.\test2.clixml'
 
This example creates a new Forte configuration by copying the current one, overriding its description and saving it to a new profile path.
#>

function New-AnyChainProfile
{
    [CmdletBinding()]
    param
    (
        # The Forte client secret specified as an unsecured string.
        [Parameter(Mandatory=$true, ParameterSetName='UnsecureSecret', ValueFromPipelineByPropertyName)]
        [Alias('secret','client_secret')]
        [string] $UnsecuredSecret,

        # The Forte client secret. To be prompted securely, either leave this unspecified or use Read-Host with the -AsSecureString parameter.
        [Parameter(Mandatory=$false, ParameterSetName='SecureSecret', ValueFromPipelineByPropertyName)]
        [SecureString] $ClientSecret,

        # The Forte application ID.
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName)]
        [Alias('application_id','client_id','id')]
        [string] $ApplicationId,

        # The base URL of the Forte environment.
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName)]
        [Alias('base_url')]
        [Uri] $BaseUrl,

        # A description of the profile.
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName)]
        [string] $Description,

        # The optional path to the Forte OpenAPI specification file (e.g., `anychain_api.json`).
        # This is used to provide argument auto-completion for Forte endpoints, verbs, and bodies.
        [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf -IsValid })]
        [ValidateScript({ [IO.Path]::GetExtension($_) -in '.yaml','.json' })]
        [string] $AnyChainOpenAPISpecPath,

        # Use the imported profile as the active profile.
        [switch] $Use
    ) 

    Process
    {
        # If a secret was not specified, prompt for one.
        if ( -not $ClientSecret -and -not $UnsecuredSecret ) 
        {
            $ClientSecret = (Read-Host -Prompt "Specify the app's client secret" -AsSecureString)
        }

        # Define the profile explicitly from parameters
        $config = [PSCustomObject] @{
            # Version = $MyInvocation.MyCommand.Module.Version
            Description = $Description
            ClientSecret = $ClientSecret ?? ($UnsecuredSecret | ConvertTo-SecureString -AsPlainText) 
            BaseUrl = [Uri] $BaseUrl
            ApplicationId = [string] $ApplicationId
            AnyChainOpenAPISpecPath = [string] $AnyChainOpenAPISpecPath
        }

        # If specified, set the profile as active.
        if ( $Use ) { $config | Use-AnyChainProfile } 
        
        # Return the profile
        $config
    }
}


<#
.SYNOPSIS
Sets the specified AnyChain profile to be used by subsequent AnyChain operations.
 
.EXAMPLE
Import-AnyChainProfile -Path <profile-path>.clixml | Use-AnyChainProfile
 
This example imports an AnyChain profile and pipes it to Use-AnyChainProfile to make it active.
#>

function Use-AnyChainProfile
{
    [CmdletBinding()]
    param
    (
        # The AnyChain profile to set as active.
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [PSCustomObject] $ProfileObject
    )

    Process
    {
        # Import the configuration and make it available internally within this module
        New-Variable -Name AnyChainProfile -Option ReadOnly -Scope Script -Value $ProfileObject -Force
        Write-Verbose -Message ($AnyChainProfile | Out-String)

        # Because there are endpoints that require the app_id be specified,
        # we'll make it available as a read-only variable.
        New-Variable -Name app_id -Value ($AnyChainProfile.ApplicationId) -Option ReadOnly,AllScope -Scope Global -Force -Description 'The application ID of the current AnyChain application'
        New-Variable -Name AnyChainAppID -Value ($AnyChainProfile.ApplicationId) -Option ReadOnly,AllScope -Scope Global -Force -Description 'The application ID of the current AnyChain application'

        # If the OpenAPI spec was provided, parse and store it.
        if ( $AnyChainProfile.AnyChainOpenAPISpecPath )
        {
            # Is the OpenAPI specified in a YAML or JSON file?
            $openAPISpec = switch ( [System.IO.Path]::GetExtension( $AnyChainProfile.AnyChainOpenAPISpecPath ) )
            {
                '.yaml' 
                {
                    # Import the Forte OpenAPI spec in YAML format
                    Get-Content -Path $ExecutionContext.InvokeCommand.ExpandString($AnyChainProfile.AnyChainOpenAPISpecPath) -Raw | convertfrom-yaml 
                }

                '.json'
                {
                    # Import the Forte OpenAPI spec in JSON format
                    Get-Content -Path $ExecutionContext.InvokeCommand.ExpandString($AnyChainProfile.AnyChainOpenAPISpecPath) -Raw | ConvertFrom-Json -NoEnumerate -AsHashtable 
                }
            }

            # Store the OpenAPI specification in a variable for internal use implementing argument completion.
            # To allow users to view and navigate the OpenAPI specification, make it available via a variable.
            New-Variable -Name AnyChainOpenAPISpecification -Scope Global -Option ReadOnly,AllScope -Value $openAPISpec -Force
            Write-Verbose -Message $AnyChainOpenAPISpecification
        }
        else 
        {
            # Ensure previously assigned OpenAPI specification is removed.
            Remove-Variable -Name AnyChainOpenAPISpecification -Scope Global -Force -ErrorAction SilentlyContinue
        }
    }
}

<#
.SYNOPSIS
Returns the currently active AnyChain profile.
#>

function Get-AnyChainProfile
{
    $AnyChainProfile
}


<#
.SYNOPSIS
Imports the specified AnyChain profile from a CLIXML file and optionally uses it.
 
.EXAMPLE
Import-AnyChainProfile -Path <profile-path>.clixml
 
This example imports an AnyChain profile.
 
.EXAMPLE
Import-AnyChainProfile -Path <profile-path>.clixml -Use
 
This example imports an AnyChain profile and specifies the -Use flag to make it active.
#>

function Import-AnyChainProfile
{
    [CmdletBinding()]
    param 
    (
        # The file from which to import the AnyChain profile.
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf -IsValid })]
        [string] $Path,

        # Use the imported profile as the active profile.
        [switch] $Use,

        # Do not return the imported profile.
        [switch] $Silent
    )

    Process
    {
        # Import the configuration and make it available internally within this module
        $config = Import-Clixml -Path $Path
        Write-Verbose -Message $config

        # If specified, set the profile as active.
        if ( $Use ) { $config | Use-AnyChainProfile } 
        
        # Return the profile
        if ( -not $Silent ) { $config }
    }
}

<#
.SYNOPSIS
Exports an AnyChain profile to a CLIXML file.
 
.NOTES
The client secret will be persisted as a SecureString, which is securely encrypted
and accessible only to the user account that created the CLIXML file.
#>

function Export-AnyChainProfile
{
    [CmdletBinding()]
    Param 
    (
        # The AnyChain profile to be exported.
        [Parameter(Mandatory=$false,ValueFromPipeline=$true)]
        [PSCustomObject] $Profile,

        # The configuration values for the application specified in a CLIXML file (.clixml).
        [Parameter(Mandatory=$true)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf -IsValid })]
        [string] $Path
    )

    Process
    {
        # Export the configuration to a CLIXML file.
        # Note that this stores the client secret encrypted, accessible only to the current user.
        $Profile ?? $AnyChainProfile | Export-Clixml -Path $Path -Force -Depth 25
    }
}


<#
.SYNOPSIS
Return the Forte OpenAPI specification for the current profile.
#>

function Get-AnyChainOpenApiSpecification
{
    $AnyChainOpenAPISpecification
}


<#
.SYNOPSIS
Get an access token that can be used to access other Forte AnyChain API endpoints.
#>

function New-AnyChainAccessToken
{
    [CmdletBinding()]
    param 
    ( 
        # Indicates the access token should not be returned, though still used for future requests.
        [switch] $Silent,
    
        # Indicates one or more requested scopes (app, admin, and/or console) to include in the token request.
        [Parameter(DontShow)]
        [ValidateSet('app','admin','console')]
        [string[]] $Scope = @( 'app' )
    )

    # Construct the AnyChain request to obtain an access token.
    $body = @{ 
        grant_type = 'client_credentials'
        scope = $Scope -join ' '
        client_id =  $AnyChainProfile.ApplicationId 
        client_secret = $AnyChainProfile.ClientSecret | ConvertFrom-SecureString -AsPlainText
    } 
    $headers = @{ Accept = "application/json" } 
# if ( $Scope -eq 'console' ) { $headers['console-request-id'] = (New-Guid).Guid } # TODO: Experimenting; not a final implementation

    # Invoke the AnyChain request to obtain an access token.
    $response = Invoke-RestMethod -Uri "$($AnyChainProfile.BaseUrl)/token" -Method POST -Headers $headers -ContentType 'application/x-www-form-urlencoded' -Body $body 
    
    # Store the token (as a SecureString) and its approximate expiration time
    New-Variable -Name AccessTokenExpiration -Option ReadOnly -Scope Script -Value ([DateTime]::Now.AddSeconds($response.expires_in)) -Force
    New-Variable -Name AccessToken -Option ReadOnly -Scope Script -Value (ConvertTo-SecureString -String $response.access_token -AsPlainText -Force) -Force 
    #New-Variable -Name AccessTokenInfo -Option ReadOnly,AllScope -Scope Global -Value $response -Force
    
    # Return the unencrypted token
    Write-Verbose -Message "New AnyChain access token: $($response.access_token)"
    if ( -not $Silent ) { $response.access_token }
}

<#
.SYNOPSIS
Revoke the current access token so it can no longer be used.
#>

function Revoke-AnyChainAccessToken
{
    [CmdletBinding()]
    Param
    (
        # Indicates the access token should not be returned.
        [switch] $Silent
    )

    # Set the token's expires_in to zero
    Invoke-AnyChain -Method post -Endpoint /token/expire | Where-Object -FilterScript { -not $Silent }

    # Invalidate the cached token information
    $AccessToken = $null
    #$AccessTokenInfo = $null
    $AccessTokenExpiration = [DateTime]::MinValue
}

<#
.SYNOPSIS
Gets the expiration time of the current token.
#>

function Get-AnyChainAccessTokenExpiration
{
    $AccessTokenExpiration
}

<#
.SYNOPSIS
Invoke an AnyChain endpoint.
 
.DESCRIPTION
The Invoke-AnyChain cmdlet submits an HTTP request to the AnyChain web service. The HTTP verb, URL (with embedded path parameters), query parameters, and body are all specified via parameters.
 
It will use the most recently acquired access token unless `-NewToken` is specified, in which case a new token will be requested and used.
 
To retrieve the results of an asynchronous endpoint, pass the operation_id value returned by this cmdlet to Get-AnyChainAsyncNotification (which can also accept the operation_id via the pipeline).
 
.LINK
https://github.com/fortelabsinc/AnyChainPSModule/wiki/using-the-anychain-module
 
.EXAMPLE
Invoke-AnyChain -Method get -Endpoint "/users/73d3ca2a-55ed-4677-9d8b-cca7941b18de" -NewToken
 
This example retrieves a new token and gets details for user ID 73d3ca2a-55ed-4677-9d8b-cca7941b18de.
 
.EXAMPLE
Invoke-AnyChain -Method get -Endpoint /nonfungibles -NewToken -ExpandProperty nonfungible_types
 
This example gets all the nonfungbiles and returns the list of nonfungible types.
 
.EXAMPLE
Invoke-AnyChain -Method get -Endpoint /marketplaces/list-price -NewToken -ExpandProperty marketplaces |
    foreach { Invoke-AnyChain -Method get -Endpoint "/marketplaces/list-price/$($_.id)/listings" -ExpandProperty listings }
 
This example gets a list of marketplaces, the retrieves the listings for each marketplace.
 
.EXAMPLE
Invoke-AnyChain -Method post -Endpoint /nonfungibles -NewToken `
    -Body @{ contract_detail = @{ chain_instance_id='33107d1e-a585-4e89-b772-4ebc8a0d0d0f' }; name = "Mabel's NFT type"; symbol = "MabelNFT" }
 
This example defines a nonfungible type as a hashtable and uses it as the body of a request to create that type.
 
.EXAMPLE
Invoke-AnyChain -Method post -Endpoint /nonfungibles -NewToken `
    -Body @{ contract_detail = @{ chain_instance_id='33107d1e-a585-4e89-b772-4ebc8a0d0d0f' }; name = "Dipper's NFT type"; symbol = "DipperNFT" } `
  | Wait-AnyChainAsyncNotification -ResponseOnly
 
This example creates a new nonfungible type, which is an asynchronous operation, then uses the Wait-AnyChainAsyncNotification to wait for the asynchronous request to complete and returns the async response, which contains the new nonfungible type.
 
.EXAMPLE
Invoke-AnyChain -Method get -Endpoint /users -ExpandProperty users -NewToken:$((Get-AnyChainAccessTokenExpiration) -lt (Get-Date))
 
This example retrieves a new token *if and only if* the current token has expired, then retrieves a list of users.
#>

function Invoke-AnyChainEndpoint
{
    [CmdletBinding(SupportsShouldProcess=$true,DefaultParameterSetName='UserToken')]
    [Alias('Invoke-AnyChain')]
    [Alias('iac')]
    Param
    (
        # Indicates that a new access token should be generated for this call.
        [Parameter(Mandatory=$false,ParameterSetName='UserToken')]
        [switch] $NewToken,

        # Indicates that a new admin access token should be generated for this call.
        [Parameter(Mandatory=$true,ParameterSetName='AdminToken')]
        [switch] $NewAdminToken,

        # The HTTP method to use (i.e., Get, Post, Patch, or Delete).
        [Parameter(Mandatory=$true,Position=0)]
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [ValidateScript({ $_ -in [Enum]::GetNames([Microsoft.PowerShell.Commands.WebRequestMethod]) })]
        [Alias('Verb')]
        [string] $Method,

        # The AnyChain REST endpoint (after the configured base URL).
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [Alias('Path')]
        [string] $Endpoint,

        # The endpoint's query parameters, either as a URI query string or a hashtable of name/value pairs.
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        $QueryParams,

        # The endpoint's body specified as a JSON string, or as a hashtable of name/value pairs
        # which will be converted to a JSON string.
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        $Body,

        # Expand the specified property of the returned object(s).
        # This is comparable to piping the results to the Select-Object cmdlet and specifying its ExpandProperty parameter.
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [string] $ExpandProperty, 

        # Indicates that multiple requests should be processed to ensure all pages of data
        # have been retrieved.
        [switch] $All,

        # Indicates that the request should be made as a web request instead of a REST request.
        # This is primarily for debugging or if the caller wants access to the HTTP response information
        # in addition to the REST response data.
        [switch] $UseWebRequest
    )

    # If requested, get a new access token.
    if ( $NewToken -or $NewAdminToken )
    { 
        New-AnyChainAccessToken -Silent -Scope:($NewAdminToken ? 'admin' : 'app')
    } 

    # If necessary, convert the body to JSON
    if ( $Body -is [System.Collections.IDictionary] ) 
    { 
        $Body = $Body | ConvertTo-Json -Compress -Depth 50 
    }

    # Ensure query string is represented as a hashtable
    if ( $QueryParams -is [string] )
    {
        $QueryParams = $QueryParams -split '&' -join "`n" | ConvertFrom-StringData
    }

    # If all pages were requested (and this is a Get), request multiple pages of results.
    if ( $All -and $Method -eq 'get' )
    {
        if ( -not $QueryParams ) { $QueryParams = @{} }
        if ( -not $queryParams.ContainsKey('cursor') ) { $QueryParams['cursor'] = 1 }
        if ( -not $queryParams.ContainsKey('per_page') ) { $QueryParams['per_page'] = 100 }
    }

    do
    {
        # Convert $QueryParams into a URL query string
        [string] $query = if ( $QueryParams -and $QueryParams.Count ) 
        { 
            # Generate the query string from the specified QueryParams dictionary
            $QueryParams.GetEnumerator() | 
                ForEach-Object { "{0}={1}" -f [Web.HttpUtility]::UrlEncode($_.Name), [Web.HttpUtility]::UrlEncode($_.Value) } | 
                Join-String -Separator '&'
        }

        # Construct and process the REST request
        $headers = @{ Accept = "application/json" } 

        # Construct the complete URL by combining the URI and the query string
        $uri = [UriBuilder]::new( $AnyChainProfile.BaseUrl.ToString().TrimEnd('/') + '/' + $Endpoint.TrimStart('/') )
        $uri.Query = $query

        # Write verbose description of REST request
        "Invoking AnyChain request: {0}" -f (@{ uri = $uri.Uri; method = $Method; headers = $headers; body = $body } | ConvertTo-Json -Compress) | Write-Verbose

        # Check for -WhatIf or -Confirm flags
        $response = if ( $PSCmdlet.ShouldProcess( $Method + " " + $uri.Uri + " " + $Body ) )
        {
            # Invoke the AnyChain request (using either Invoke-WebRequest or Invoke-RestMethod)
            if ( $UseWebRequest ) {
                Invoke-WebRequest -Uri $uri.Uri -Method $Method -Headers $headers -ContentType 'application/json' -Body $Body -Authentication Bearer -Token $AccessToken
            }
            else {
                Invoke-RestMethod -Uri $uri.Uri -Method $Method -Headers $headers -ContentType 'application/json' -Body $Body -Authentication Bearer -Token $AccessToken 
            }
        }

        # Forward web request result to the stream. If requested, expand the specified property.
        $response | Select-Object -ExpandProperty $ExpandProperty 

        # If retrieving all pages, look for the next page. Break out if none specified.
        if ( $All -and $Method -eq 'get' )
        {
            try { $QueryParams['cursor'] = $response.pagination.next_cursor } catch { break }
            $NewToken = $false
        }
    }
    while ( $All )
}

<#
.SYNOPSIS
This cmdlet returns the latest notification (operation and response) for the specified async operation,
polling every WaitMilliseconds milliseconds waiting for the operation to either complete or fail.
 
.NOTES
This is a convenience function to provide simple and easy access to the result of an asynchronous endpoint.
It does not support paging or returning results for more than one operation.
To access the full functionality of the /apps/{app_id}/notifications endpoint, use the Invoke-AnyChain
cmdlet to access it explicitly.
 
.EXAMPLE
Invoke-AnyChain -Method post -Endpoint /nonfungibles -NewToken `
    -Body @{ contract_detail = @{ chain_instance_id='33107d1e-a585-4e89-b772-4ebc8a0d0d0f' }; name = "Dipper's NFT type"; symbol = "DipperNFT" } `
  | Wait-AnyChainAsyncNotification -ResponseOnly
 
This example creates a new nonfungible type, which is an asynchronous operation, then uses the Wait-AnyChainAsyncNotification to wait for the asynchronous request to complete and returns the async response, which contains the new nonfungible type.
 
.EXAMPLE
Invoke-AnyChain -Method post -Endpoint /nonfungibles -NewToken `
    -Body @{ contract_detail = @{ chain_instance_id='33107d1e-a585-4e89-b772-4ebc8a0d0d0f' }; name = "Mabel's NFT type"; symbol = "MabelNFT" } `
  | Wait-AnyChainAsyncNotification -ResponseOnly -ExpandProperty nonfungible_type
 
This example uses the Wait-AnyChainAsyncNotification to wait for the asynchronous request to complete and returns the nonfungible_type property of the async response.
 
#>

filter Wait-AnyChainAsyncNotification
{
    [Alias('wac')]
    [CmdletBinding()]
    Param
    (
        # The operation for which a response is requested.
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName)]
        [Alias('operation_id')] 
        [string] $OperationID,

        # Return the response(s) instead of the full notification.
        [switch] $ResponseOnly,

        # Expand the specified property of the returned object(s).
        # This is comparable to piping the results to the Select-Object cmdlet and specifying its ExpandProperty parameter.
        [string] $ExpandProperty,

        # How frequently (in milliseconds) to poll for a result. To return without waiting, specify zero (0) milliseconds.
        [int] $WaitMilliseconds = 100,

        # The application from which to retrieve the response(s) (default to ApplicationId of current profile).
        [string] $ApplicationId
    )

    Process
    {
        # Initialize the application ID (when done as a parameter initialization, it acts like a breakpoint when debugging)
        if ( -not $ApplicationId ) { $ApplicationId = $AnyChainProfile.ApplicationId }

        # Retrieve the async notification
        $getNewToken = try { ( (Get-Date).AddMilliseconds(10000) -gt $AccessTokenExpiration ) } catch { $true }
        $rawResponse = Invoke-AnyChain -Endpoint "/apps/$ApplicationId/notifications" -Method get -NewToken:$getNewToken -QueryParams @{ operation_id = $OperationID } 

        # If the operation is still pending and a wait interval was requested (the default), sleep and try again.
        while ( $WaitMilliseconds -and ($rawResponse.notifications | Sort-Object -Bottom 1 -Property sequence_number).operation.status -eq 'pending' )
        {
            # Give the operation some time to complete
            Start-Sleep -Milliseconds $WaitMilliseconds

            # Check the status of the operation again, requesting a new token if within 10 seconds of the current token's expiration.
            $getNewToken = ( (Get-Date).AddMilliseconds(10000) -gt $AccessTokenExpiration )
            $rawResponse = Invoke-AnyChain -Endpoint "/apps/$ApplicationId/notifications" -Method get -NewToken:$getNewToken -QueryParams @{ operation_id = $OperationID }
        } 

        # Return either the latest response or the latest notification, depending on the ResponseOnly parameter.
        $rawResponse.notifications | Sort-Object -Property sequence_number -Bottom 1 | ForEach-Object { $ResponseOnly ? $_.response : $_ | Select-Object -ExpandProperty $ExpandProperty }
    }
}



<#
.SYNOPSIS
Gets the AnyChain definition for the specified endpoint/method pair.
If External is requested, open the reference web page for this command.
#>

function Get-AnyChainEndpointSpecification 
{
    Param
    (
        # The AnyChain REST endpoint (after the configured base URL).
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [Alias('Path')]
        [string] $Endpoint,

        # The HTTP method to use (i.e., Get, Post, Patch, or Delete).
        [Parameter(Mandatory=$true)]
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [ValidateScript({ $_ -in [Enum]::GetNames([Microsoft.PowerShell.Commands.WebRequestMethod]) })]
        [Alias('Verb')]
        [string] $Method,

        # Open the external help web page (as specified in the API specification as 'externalDocs.url' under the $Endpoint/$Method-specified 'paths' property).
        [switch] $External
    )

    if ( $External ) 
    { 
        try { Start-Process -FilePath $AnyChainOpenAPISpecification.paths[ $Endpoint][ $Method ].externalDocs.url.ToLower() } catch { }
    } 

    $AnyChainOpenAPISpecification.paths[ $Endpoint][ $Method ] 
}

<#
.SYNOPSIS
Returns the specified OpenAPI specification "reference" from the AnyChain specification.
A "reference" is an OpenAPI element that contains a $ref property, which indicates
another element in the specification which defines the current referencing element.
 
.EXAMPLE
(Get-AnyChainEndpointSpecification -Endpoint /fungibles -Method post).requestBody | Get-AnyChainOpenApiReference
#>

function Get-AnyChainOpenApiReference
{
    Param
    ( 
        [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $Element
    )
    
    Process
    {
        # If the OpenAPI element contains a reference ($ref), dereference it. Otherwise, return the element.
        if ( $Element -is [System.Collections.IDictionary] -and $Element.Keys -contains '$ref' )
        {
            # Find the path to the referenced element and retrieve it.
            $target = DerefOASItem -RefItem $Element

            # If the referencing element has properties, add them.
            $target += $Element 
            $target.Remove('$ref')
            $target
        }
        else 
        {
            $Element
        } 
    }
}


<#
.SYNOPSIS
Returns a list of AnyChain endpoints along with their descriptions, query parameters, and
bodies as defined in the OpenAPI specification. If one or more Methods and/or Endpoints are specified,
it will return endpoints that match the specified criteria. Wildcards are allowed for endpoints.
#>

function Get-AnyChainEndpoint
{
    [CmdletBinding()]
    param 
    (
        # The HTTP method(s) to include (i.e., Get, Post, Patch, or Delete).
        # Default is to include all of them.
        [Parameter(Mandatory=$false,ValueFromPipelineByPropertyName)]
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [ValidateScript({ $_ -in [Enum]::GetNames([Microsoft.PowerShell.Commands.WebRequestMethod]) })]
        [Alias('Verb')]
        [string[]] $Method = [Enum]::GetNames([Microsoft.PowerShell.Commands.WebRequestMethod]),

        # The AnyChain REST endpoint.
        [Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [ArgumentCompleter( { GetEndpointArguments @args } )]
        [Alias('Path')]
        [string] $Endpoint
    )

    Process
    {
        # Retrieve all endpoints that match the specified endpoint name and method/verb.
        $AnyChainOpenAPISpecification.paths.GetEnumerator() | ForEach-Object `
        { 
            $path = $_.Key

            if ( -not $Endpoint -or $path -like $Endpoint ) 
            {
                $_.Value.GetEnumerator() | ForEach-Object `
                { 
                    $verb = $_.Key
                    if ( $verb -in $Method -and $verb -in [Enum]::GetNames([Microsoft.PowerShell.Commands.WebRequestMethod]) ) 
                    { 
                        $body = Get-EndpointBody -Method $verb -Endpoint $path
                        $qParams = Get-EndpointQueryParams -Method $verb -Endpoint $path
                        $response = Get-EndpointResponse -Method $verb -Endpoint $path
                        [PSCustomObject] @{ Verb=$verb; Path=$path; Summary = $_.Value.summary; Description = $_.Value.description; Body = $body; QueryParams = $qParams; Response = $response } 
                    } 
                }
            } 
        } 
    }
}