Build/PSA.AtProtocol.Build.ps1

# Push to the parent directory of this script
Push-Location ($PSScriptRoot | Split-Path)

# Update remote submodules
git submodule update --remote | Out-Host

# The AtProtocol is a submodule of this repo, located within 'atproto'
$atRoot = Join-Path $pwd atproto
# Within that are lexicons (these describe the types in At Protocol in JSON)
$atLexicon = Join-Path $atRoot lexicons
# Get all of the json files beneath this directory.
$lexiconJson = Get-ChildItem -Path $atLexicon -Recurse -file -Filter *.json

$atFunctionNames = @()
$Lexicons = @()

$AtScriptRoot = Join-Path (Join-Path $pwd Commands) "Lexicons"

if (-not (Test-Path $AtScriptRoot)) {
    $null = New-Item -ItemType Directory -Path $AtScriptRoot
}

# The At Protocol could stand a bit better documentation of inputs and their purpose
# However, we can predefine help for several parameters that will exist in a variety of commands.
$atParameterHelp = 
    [Ordered]@{
        Actor  = "
The Actor.

This can be either a handle (e.g. @AtProto.com) or a Decentralized Identifier (.did)"


        Cursor = "
A cursor that can be used to get more results.

Any command that accepts a -Cursor parameter returns a .Cursor property.

You can provide this -Cursor to the same command with the same input to get more results.
"

        Limit = "A limit to the number of results returned."
        Did   = "The Decentralized Identifier. This is a uniqueID used throughout the At Protocol."
    }

$script:LastDefinitionFile = $null
$Script:LastDefinitionLexiconId = $null
function ResolveAtRefs {
    param($PropertyName, $refToFind)
    
    
    $decorateProperty = [Ordered]@{}
    if (-not $refToFind) { return $decorateProperty }
    $decorateProperty[$PropertyName] = $refToFind

    $propertyCollection = $null 
    if ($executionContext.SessionState.InvokeCommand.GetCommand($refToFind,'Alias')) {
        $reference = & $refToFind
        if (-not $reference) { return $decorateProperty }
        if ($reference.defs) {
            $script:LastDefinitionFile = $reference
            $refWithinDef = @($refToFind -split '#',2)[-1]
            if ($reference.defs.($refWithinDef)) {
                $reference = $reference.defs.($refWithinDef)
            }
        }
        $propertyCollection = $reference.properties.psobject.properties        
    }
    elseif ($lexicon.defs.($refToFind -replace '^#')){
        $Script:LastDefinitionLexiconId = $lexicon.id
        $propertyCollection = $lexcion.defs.($refToFind -replace '^#').properties.psobject.properties
    }
    elseif ($script:LastDefinitionFile.defs.($refToFind -replace '^#')) {
        $Script:LastDefinitionLexiconId = $script:LastDefinitionFile.id
        $propertyCollection = $script:LastDefinitionFile.defs.($refToFind -replace '^#').properties.psobject.properties
    }

    foreach ($referrencedProperty in $propertyCollection) {
        if (-not $referrencedProperty.value.ref) { continue }
        $recursiveRefs = ResolveAtRefs $referrencedProperty.Name $referrencedProperty.value.ref
        if ($recursiveRefs.Count) {
            foreach ($kv in $recursiveRefs.GetEnumerator()) {
                $decorateProperty["$($PropertyName).$($kv.Key)"] = 
                    if ($kv.Value -match '^#' -and $Script:LastDefinitionLexiconId) {
                        $Script:LastDefinitionLexiconId + $kv.Value
                    } else {
                        $kv.Value
                    }
            }
        }            
    }

    return $decorateProperty
}

$unbound = @()
foreach ($lexiconFile in $lexiconJson) {
    $lexiconText = Get-content -LiteralPath $lexiconFile -raw 
    $lexicon = $lexiconText | ConvertFrom-Json
    $Lexicons+=$lexicon
    $lexiconIdParts = @($lexicon.id -split '\.')
    $secondWord = $lexiconIdParts[1]
    $lastWord = $lexiconIdParts[-1]
    $secondToLastWord = $lexiconIdParts[-2]
    $secondToLastWord = $secondToLastWord.Substring(0,1).ToUpper() + $secondToLastWord.Substring(1)
    $secondWord = $secondWord.Substring(0,1).ToUpper() + $secondWord.Substring(1)
    $prefix = "$($secondWord)$($secondToLastWord)"


    

    if ($lexiconFile.Name -eq 'defs.json') {
        # General definitions

        # This is a whole other beast.

        # The easiest and most flexible path here is to make this it's own category of command, one that returns itself.


        $atFunctionName = "Get-${Prefix}Definition"
        $atFunctionName = $atFunctionName -creplace "-Atproto", "-AtProto"
        $atFunctionDefinition = New-PipeScript -End ([scriptblock]::Create("
`$lexiconText = @'
$($lexiconText)
'@
`$lexicon = `$lexiconText | ConvertFrom-JSON
if (`$myInvocation.InvocationName -eq `$myInvocation.MyCommand.Name) {
    `$lexicon
} elseif (`$myInvocation.InvocationName -like '*#*') {
    `$lexicon.defs.`$(@(`$myInvocation.InvocationName -split '\#',2)[1])
} else {
    `$lexicon
}
"
)) -FunctionName $atFunctionName -Alias @(
    # And give each an alias with the last 3 words in the lexicon
    "$($lexiconIDParts[1..$($lexiconIDParts.Count - 1)] -join '.')"
    # as well as the whole identifier
    "$($lexiconIDParts -join '.')"
    foreach ($prop in $lexicon.defs.psobject.Properties) {
        "$($lexiconIDParts -join '.')#$($prop.Name)"
    }
)

        $atFunctionPath = Join-Path $AtScriptRoot "$($lexiconIDParts[0..2] -join [IO.Path]::DirectorySeparatorChar)$([IO.Path]::DirectorySeparatorChar)$($atFunctionName).ps1"
        if (-not (Test-Path $atFunctionPath)) {
            $null = New-Item -ItemType File -Path $atFunctionPath -Force
        }

        

        $atFunctionDefinition | Set-Content -Path $atFunctionPath
        Get-Item -path $atFunctionPath


    } 
    else {
        $httpMethod = 
            if ($lexicon.defs.main.type -eq 'procedure') {
                "POST"        
            } elseif ($lexicon.defs.main.type -eq 'query') {
                "GET"
            } 
            elseif ($lexicon.defs.main.type -in 'record','object') {
                continue
            }
            elseif (-not $lexicon.defs.main) {                                
                continue
            }

        

        $AtParams = [Ordered]@{}        
        
        $atSourceProperties = @(
            $lexicon.defs.main.parameters.properties.psobject.Properties
            $lexicon.defs.main.input.schema.properties.psobject.Properties
        ) -ne $null

        $atRequired = @(
            $lexicon.defs.main.parameters.required
            $lexicon.defs.main.input.schema.properties.required
        ) -ne $null
        
        foreach ($AtProperty in $atSourceProperties) {
            $AtParam = $AtParams["$($AtProperty.Name.Substring(0,1).ToUpper() + $AtProperty.Name.Substring(1))"] = [Ordered]@{}
            $AtParam.Attribute = @()
            if ($AtProperty.Value.Description) {
                $AtParam.Description = $AtProperty.Value.Description
            } elseif ($atParameterHelp[$AtProperty.Name]) {
                $AtParam.Description = $atParameterHelp[$AtProperty.Name]
            }
            if ($atRequired -contains $AtProperty.Name) {
                $AtParam.Attribute += "Mandatory"
            }
            $AtParam.Attribute += "ValueFromPipelineByPropertyName"
            $AtParam.Binding = $AtProperty.Name
            $AtParam.Type = 
                switch ($AtProperty.value.type) {
                    string { [string] }
                    boolean { [switch] }
                    default { [PSObject]}
                }
        }



        
        $atFunctionName = switch -regex ($lastWord) {
            "^(?>Get|Resolve|Revoke|Reset|Request|Search|Send|Enable|Disable|Update|Block|Register|Unregister)" {
                $newName = $lastWord -replace "^$($matches.0)", "`${0}-$prefix"
                $newName.Substring(0,1).ToUpper() + $newName.Substring(1)
            }
            "^Create" {
                $lastWord -replace "^Create", "Add-$prefix"
            }
            "^Delete" {
                $lastWord -replace "^Delete", "Remove-$prefix"
            }
            "^Describe" {
                $lastWord -replace "^Describe", "Get-$prefix"
            }
            "^List" {
                $lastWord -replace "^List", "Get-$prefix"
            }
            "^Upgrade" {
                $lastWord -replace "^Upgrade", "Update-$prefix"
            }
            "^Query" {
                $lastWord -replace "^Query", "Search-$prefix"
            }
            "^Mute" {
                $lastWord -replace "^Mute", "Block-$prefix"
            }
            "^Unmute" {
                $lastWord -replace "^Unmute", "Unblock-$prefix"
            }            
            "^Subscribe" {
                $lastWord -replace "^Subscribe", "Watch-$prefix"
            }
            "^Reverse" {
                $lastWord -replace "^Reverse", "Undo-$prefix"
            }
            "^Put" {
                $lastWord -replace "^Put", "Set-$prefix"
            }
            "^Follow" {
                $lastWord -replace "^Follow", "Watch-$prefix"
            }
            '^Refresh' {
                $lastWord -replace '^Refresh', "Sync-$prefix"
            }
            '^NotifyOf' {
                $lastWord -replace '^NotifyOf', "Watch-$prefix"
            }
            '^Upload' {
                $lastWord -replace '^Upload', "Set-$prefix"
            }
            '^Take' {
                $lastWord -replace '^Take', "Invoke-$prefix"
            }
            '^Apply' {
                $lastWord -replace '^Apply', "Set-$prefix"
            }
        }

        if (-not $atFunctionName -or $atFunctionName -notlike '*-*') {
            $unbound += $lexicon

            # "No Verb Found $($lexicon.id)" | Out-Host
            continue
        } else {
            $atFunctionName = $atFunctionName -replace "(?:$secondToLastWord){2}",$secondToLastWord
            $atFunctionNames += $atFunctionName
        }

        if ((-not $AtParams.Count) -and $lexicon.defs.main.input.encoding) {
            $AtParams["Data"] = [Ordered]@{
                Binding = "."
                Attribute = 'ValueFromPipelineByPropertyName'
                Type = [byte[]]
            }

            $AtParams["ContentType"] = [Ordered]@{
                Attribute = 'ValueFromPipelineByPropertyName'
                Type = [string]
            }
        }


        $decorateProperty = [Ordered]@{}
        foreach ($outputProperty in $lexicon.defs.main.output.schema.properties.PSObject.Properties) {
        
            # If the output property had decoration, we want to carry it down
            $refToFind = 
                if ($outputProperty.Value.ref) {
                    $outputProperty.Value.ref
                } elseif ($outputProperty.Value.items.ref) {
                    $outputProperty.Value.items.ref
                }
            if (-not $refToFind) { continue }
            $resolvedRefProps = ResolveAtRefs $outputProperty.Name $refToFind
            if ($resolvedRefProps -is [Collections.IDictionary]) {
                $decorateProperty += $resolvedRefProps
            } else {
                $null = $null
            }                        
        }        

        $atBeginBlock = [ScriptBlock]::Create(@(
            "`$NamespaceID = '$($lexicon.id)'"
            "`$httpMethod = '$httpMethod'"
            "`$InvokeAtSplat = [Ordered]@{Method=`$httpMethod}"
            if ($decorateProperty.Count) {
                "`$InvokeAtSplat.DecorateProperty = [Ordered]@{
$(@(foreach ($kv in $decorateProperty.GetEnumerator()) {
    " '$($kv.Key)'='$($kv.Value)'"
}) -join [Environment]::NewLine)
}"

            }
            {$InvokeAtSplat["PSTypeName"] = $NamespaceID}
            {$parameterAliases = [Ordered]@{}}
            {$DataboundParameters = @()}
            if ($lexicon.defs.main.output.encoding -and ($lexicon.defs.main.output.encoding -ne 'application/json')) {
                {$AsByte = $true}
            } else {
                {$AsByte = $false}
            }
            
            ""
if ($AtParams.Count) {
{
    
:nextParameter foreach ($paramMetadata in 
    ([Management.Automation.CommandMetadata]$MyInvocation.MyCommand).Parameters.Values) {
    
    foreach ($attr in $paramMetadata.Attributes) {
        if ($attr -is [ComponentModel.DefaultBindingPropertyAttribute]) {
            $parameterAliases[$paramMetadata.Name] = $attr.Name
            $DataboundParameters += $paramMetadata.Name
            continue nextParameter
        }
    }
}

}
}
{
    $parameterQueue = [Collections.Queue]::new()
}
        ) -join [Environment]::NewLine)
        

        $atProcessBlock = {
$parameterQueue.Enqueue([Ordered]@{} + $PSBoundParameters)            
        }

        $atEndBlock  = {
            $parameterQueue.ToArray() |
                Invoke-AtProtocol -Method $httpMethod -NamespaceID $NamespaceID -Parameter {
                    $RestParameters =[Ordered]@{}
                    foreach ($parameterName in $DataboundParameters) {
                        if ($null -ne $_.($ParameterName)) {
                            $RestParameters[$parameterName] = $_.($ParameterName)
                        }
                    }
                    $RestParameters
                } -ParameterAlias $parameterAliases @InvokeAtSplat -ContentType $(
                    if ($ContentType) {
                        $ContentType
                    } else {
                        "application/json"   
                    }
                ) -AsByte:$AsByte -Property {
                    $_
                } -Cache:$(
                    if ($cache) {$cache} else { $false }
                ) -Raw:$Raw -Authorization {
                    if ($_.Authorization) { 
                        $_.Authorization
                    } else { 
                        $null
                    }
                }
        }


        $lexiconLink = @(
            "https://github.com/bluesky-social/atproto/tree/main/lexicons"
            $lexiconIDParts[0..$($lexiconIDParts.Count - 2)]
            $lexiconIdParts[-1] + ".json"
        ) -join '/'

        $atFunctionDescription = if ($lexcion.description) {
            $lexcion.description
        } else {
            "$($lexicon.id)"
        }
        
        # Make AtProto camel case, for the preference of most PowerShell users.
        $atFunctionName = $atFunctionName -creplace "-Atproto", "-AtProto"
        $atFunctionAliases = @(
            # If the function was named -AtProto
            if ($atFunctionName -like '*-AtProto*') {
                # give it an alias with -AtProtocol
                $atFunctionName -replace "-AtProto", "-AtProtocol"
            }
            # If the function was named -Bsky
            if ($atFunctionName -like '*-Bsky*') {
                # give it an alias with -BlueSky
                $atFunctionName -replace "-Bsky", "-BlueSky"
            }

            # And give each an alias with the last 3 words in the lexicon
            "$($lexiconIDParts[1..$($lexiconIDParts.Count - 1)] -join '.')"
            # as well as the whole identifier
            "$($lexiconIDParts[0..$($lexiconIDParts.Count - 1)] -join '.')"
        )
        
        if (-not $AtParams["Cache"] -and $httpMethod -eq 'GET') {
            $AtParams["Cache"] = [Ordered]@{
                Name = "Cache"
                Help = "If set, will cache results for performance."
                ParameterType = [switch]
            }
        }

        if (-not $AtParams["Authorization"]) {
            $AtParams["Authorization"] = [Ordered]@{
                Name = "Authorization"
                Alias = 'Authentication','AppPassword','Credential','PSCredential'
                Help = "The authorization.", "This can be a JWT that accesses the at protocol or a credential.","If this is provided as a credential the username is a handle or email and the password is the app password."
                ParameterType = [switch]
            }    
        }

        if (-not $AtParams["Raw"]) {
            $AtParams["Raw"] = [Ordered]@{
                Name = "Raw"
                Help = "If set, will return raw results. This will ignore -Property, -DecorateProperty, -ExpandProperty, and -PSTypeName."
                ParameterType = [switch]
            }
        }

        $newPipeScriptSplat = [Ordered]@{
            FunctionName=$atFunctionName
            Alias= $atFunctionAliases
            Parameter=$AtParams
            Description=$atFunctionDescription
            Synopsis=$lexicon.id
            Begin = $atBeginBlock
            Process = $atProcessBlock
            End = $atEndBlock
            Link = $lexiconLink            
            Attribute = "[CmdletBinding(SupportsShouldProcess)]"
        }

        
        $atFunctionDefinition =
            New-PipeScript @newPipeScriptSplat -NoTranspile

        
        $atFunctionPath = Join-Path $AtScriptRoot "$($lexiconIDParts[0..2] -join [IO.Path]::DirectorySeparatorChar)$([IO.Path]::DirectorySeparatorChar)$($atFunctionName).ps1"
        if (-not (Test-Path $atFunctionPath)) {
            $null = New-Item -ItemType File -Path $atFunctionPath -Force
        }

        

        $atFunctionDefinition | Set-Content -Path $atFunctionPath
        Get-Item -path $atFunctionPath

        # If the parameter had a cursor, go ahead an make a .More method for that type
        if ($AtParams.Cursor) {
            $typesDirectory = Join-Path $pwd Types
            $targetDirectory =  $typesDirectory
            foreach ($part in $lexiconIdParts) {
                $targetDirectory = Join-Path $targetDirectory $part
            }
            
            $psTypeNamePath = (Join-Path $targetDirectory "PSTypeName.txt")
            if (-not (Test-Path $psTypeNamePath)) {
                $null = New-Item -ItemType File -Path $psTypeNamePath -Force
            }

            [IO.File]::WriteAllText($psTypeNamePath,"$($lexicon.id)") 

            Get-Item -Path $psTypeNamePath
            
            $MorePath = Join-path $targetDirectory "get_More.ps1"
            if (-not (Test-Path $MorePath)) {
                $null = New-Item -ItemType File -Path $psTypeNamePath -Force
            }
            Set-Content "$({
<#
.SYNOPSIS
    Gets additional results.
.DESCRIPTION
    Gets the next page of results of MORE.
#>
$this | MORE
} -replace "MORE", $(
    if ($AtParams.Cache) {
        "$($lexicon.id) -Cache"
    } else {
        $lexicon.id
    }
))"
 -Path $MorePath
            Get-Item -Path $MorePath -Force
        }
    }
    
    
}

Pop-Location