VaporShell.Classes.ps1

using namespace System
using namespace System.Collections
using namespace System.Collections.Generic
using namespace System.Collections.Specialized
using namespace System.IO
using namespace System.Management.Automation
using namespace System.Net
using namespace System.Xml
[CmdletBinding()]
Param()

Write-Verbose "Importing class 'AutoScalingProcess'"
enum AutoScalingProcess {
    Launch
    Terminate
    HealthCheck
    ReplaceUnhealthy
    AZRebalance
    AlarmNotification
    ScheduledActions
    AddToLoadBalancer
}

Write-Verbose "Importing class 'DeletionPolicy'"
enum DeletionPolicy {
    Delete
    Retain
    Snapshot
}

Write-Verbose "Importing class 'LoggingLevel'"
enum LoggingLevel {
    OFF
    ERROR
    INFO
}

Write-Verbose "Importing class 'UpdateReplacePolicy'"
enum UpdateReplacePolicy {
    Delete
    Retain
    Snapshot
}

Write-Verbose "Importing class 'VaporShellServiceModule'"
enum VaporShellServiceModule {
    AccessAnalyzer
    ACMPCA
    Alexa
    AmazonMQ
    Amplify
    ApiGateway
    ApiGatewayV2
    AppConfig
    AppFlow
    AppIntegrations
    ApplicationAutoScaling
    ApplicationInsights
    AppMesh
    AppRunner
    AppStream
    AppSync
    Athena
    AuditManager
    AutoScaling
    AutoScalingPlans
    Backup
    Batch
    Budgets
    Cassandra
    CE
    CertificateManager
    Chatbot
    Cloud9
    CloudFormation
    CloudFront
    CloudTrail
    CloudWatch
    CodeArtifact
    CodeBuild
    CodeCommit
    CodeDeploy
    CodeGuruProfiler
    CodeGuruReviewer
    CodePipeline
    CodeStar
    CodeStarConnections
    CodeStarNotifications
    Cognito
    Config
    Connect
    CUR
    CustomerProfiles
    DataBrew
    DataPipeline
    DataSync
    DAX
    Detective
    DevOpsGuru
    DirectoryService
    DLM
    DMS
    DocDB
    DynamoDB
    EC2
    ECR
    ECS
    EFS
    EKS
    ElastiCache
    ElasticBeanstalk
    ElasticLoadBalancing
    ElasticLoadBalancingV2
    Elasticsearch
    EMR
    EMRContainers
    Events
    EventSchemas
    FinSpace
    FIS
    FMS
    FraudDetector
    FSx
    GameLift
    GlobalAccelerator
    Glue
    Greengrass
    GreengrassV2
    GroundStation
    GuardDuty
    IAM
    ImageBuilder
    Inspector
    IoT
    IoT1Click
    IoTAnalytics
    IoTCoreDeviceAdvisor
    IoTEvents
    IoTFleetHub
    IoTSiteWise
    IoTThingsGraph
    IoTWireless
    IVS
    Kendra
    Kinesis
    KinesisAnalytics
    KinesisAnalyticsV2
    KinesisFirehose
    KMS
    LakeFormation
    Lambda
    LicenseManager
    Location
    Logs
    LookoutEquipment
    LookoutMetrics
    LookoutVision
    Macie
    ManagedBlockchain
    MediaConnect
    MediaConvert
    MediaLive
    MediaPackage
    MediaStore
    MSK
    MWAA
    Neptune
    NetworkFirewall
    NetworkManager
    NimbleStudio
    OpsWorks
    OpsWorksCM
    Pinpoint
    PinpointEmail
    QLDB
    QuickSight
    RAM
    RDS
    Redshift
    ResourceGroups
    RoboMaker
    Route53
    Route53RecoveryControl
    Route53RecoveryReadiness
    Route53Resolver
    S3
    S3ObjectLambda
    S3Outposts
    SageMaker
    SAM
    SDB
    SecretsManager
    SecurityHub
    ServiceCatalog
    ServiceCatalogAppRegistry
    ServiceDiscovery
    SES
    Signer
    SNS
    SQS
    SSM
    SSMContacts
    SSMIncidents
    SSO
    StepFunctions
    Synthetics
    Timestream
    Transfer
    WAF
    WAFRegional
    WAFv2
    WorkSpaces
    XRay
}

Write-Verbose "Importing class 'VSHashtable'"

class VSHashtable : OrderedDictionary {
    # Anything inheriting from this class will only show the hashtable contents.
    # Object properties will be stripped when cast to JSON/YAML.
    # Useful for adding corresponding public properties for intellisense.
    static hidden [string] $_vsFunctionName = ''
    static hidden [string] $_awsDocumentation = ''
    hidden [string[]] $_commonParams = @('Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable','InformationVariable','OutVariable','OutBuffer','PipelineVariable')

    hidden [void] _addAccessors() {}

    [object] Help() {
        return $this.Help($null)
    }

    [object] Help([string] $scope) {
        if ([string]::IsNullOrEmpty($this._vsFunctionName)) {
            return "Help content has not been created for this class. Please open an issue on the GitHub repository to request help for this class."
        }
        $params = @{Name = $this._vsFunctionName}
        switch -Regex ($scope) {
            '^F(u|ull){0,1}' {
                $params["Full"] = $true
            }
            '^D(e|etail){0,1}' {
                $params["Detailed"] = $true
            }
            '^E(x|xample){0,1}' {
                $params["Examples"] = $true
            }
            '^O(n|nline){0,1}$' {
                $params["Online"] = $true
            }
        }
        return (Get-Help @params)
    }

    [string] Docs() {
        if ([string]::IsNullOrEmpty($this._awsDocumentation)) {
            return "AWS Documentation link not found for this class!"
        }
        Start-Process $this._awsDocumentation
        return "Opening documentation link: $($this._awsDocumentation)"
    }

    [OrderedDictionary] ToOrderedDictionary() {
        return $this.ToOrderedDictionary($false)
    }

    [OrderedDictionary] ToOrderedDictionary([bool] $addAllProperties) {
        $clean = [ordered]@{}
        $this.GetEnumerator() | ForEach-Object {
            $key = if ($_.Name) {
                $_.Name
            }
            else {
                $_.Key
            }
            $value = $_.Value
            if (
                $addAllProperties -or (
                    $key -notmatch '^(_|LogicalId$)' -and (
                        $value -is [enum] -or
                        $key -match '::' -or
                        $null -ne $value
                    ) -and (
                        $value -isnot [string] -or
                        -not [string]::IsNullOrEmpty($value)
                    )
                )
            ) {
                $clean[$key] = if ($key -match '$(UpdateReplacePolicy|DeletionPolicy)$' -and $value.ToString() -match '^(Delete|Retain|Snapshot)$') {
                    (Get-Culture).TextInfo.ToTitleCase($value.ToString().ToLower())
                }
                elseif ($value -is [enum]) {
                    $value.ToString()
                }
                elseif ($value -is [IDictionary] -and $value -isnot [VSHashtable]) {
                    $value
                }
                elseif ($value | Get-Member -Name ToOrderedDictionary* -MemberType Method -ErrorAction SilentlyContinue) {
                    try {
                        $value.ToOrderedDictionary($addAllProperties)
                    }
                    catch {
                        $value
                    }
                }
                else {
                    $value
                }
                Write-Debug "Key matched: $key"
                Write-Debug "Value matched: $($clean[$key])"
            }
            else {
                Write-Debug "Key excluded: $key"
            }
        }
        return $clean
    }

    [string] ToJson() {
        return $this.ToJson($false)
    }

    [string] ToJson([bool] $compress) {
        $clean = if ($this['LogicalId']) {
            @{$this['LogicalId'] = $this.ToOrderedDictionary()}
        }
        else {
            $this.ToOrderedDictionary()
        }
        return $clean | ConvertTo-Json -Depth 50 -Compress:$compress
    }

    [string] ToYaml() {
        return $this.ToYaml($false)
    }

    [string] ToYaml([bool] $usePowerShellYaml) {
        $flipped = if (-not $usePowerShellYaml -and $null -ne (Get-Command cfn-flip* -ErrorAction SilentlyContinue)) {
            ($this.ToJson() | cfn-flip) -join [System.Environment]::NewLine
        }
        else {
            $clean = if ($this['LogicalId']) {
                @{$this['LogicalId'] = $this.ToOrderedDictionary()}
            }
            else {
                $this.ToOrderedDictionary()
            }
            ($clean | ConvertTo-Yaml) -join [System.Environment]::NewLine
        }
        return $flipped
    }

    VSHashtable() {
        $this._addAccessors()
    }

    VSHashtable([IDictionary] $props) {
        $this._addAccessors()
        Write-Debug "[$($this.GetType())] Contructing from input IDictionary"
        $props.GetEnumerator() | ForEach-Object {
            Write-Debug "[$($this.GetType())] [$($_.Key)] Checking for property"
            if ($this.GetType().FullName -eq 'VSHashtable' -or ($this.PSObject.Properties.Name -contains $_.Key -and $_.Key -notin $this._commonParams)) {
                Write-Debug "[$($this.GetType())] [$($_.Key)] Property found, adding value: $($_.Value)"
                $val = if ($_.Value -is [enum]) {
                    $_.Value.ToString()
                }
                else {
                    $_.Value
                }
                $this[$_.Key] = $val
            }
        }
    }

    VSHashtable([psobject] $props) {
        $this._addAccessors()
        Write-Debug "[$($this.GetType())] Contructing from input PSObject"
        $props.PSObject.Properties | ForEach-Object {
            Write-Debug "[$($this.GetType())] [$($_.Name)] Checking for property"
            if ($this.GetType().FullName -eq 'VSHashtable' -or ($this.PSObject.Properties.Name -contains $_.Name -and $_.Name -notin $this._commonParams)) {
                Write-Debug "[$($this.GetType())] [$($_.Name)] Property found, adding value: $($_.Value)"
                $val = if ($_.Value -is [enum]) {
                    $_.Value.ToString()
                }
                else {
                    $_.Value
                }
                $this[$_.Name] = $val
            }
        }
    }
}

Write-Verbose "Importing class 'VSObject'"

class VSObject : object {
    # Anything inheriting from this class will only show the hashtable contents.
    # Object properties will be stripped when cast to JSON/YAML.
    # Useful for adding corresponding public properties for intellisense.
    static hidden [string] $_vsFunctionName = ''
    static hidden [string] $_awsDocumentation = ''
    hidden [string[]] $_commonParams = @('Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable','InformationVariable','OutVariable','OutBuffer','PipelineVariable')

    hidden [void] _addAccessors() {}

    [object] Help() {
        return $this.Help($null)
    }

    [object] Help([string] $scope) {
        if ([string]::IsNullOrEmpty($this._vsFunctionName)) {
            return "Help content has not been created for this class. Please open an issue on the GitHub repository to request help for this class."
        }
        $params = @{Name = $this._vsFunctionName}
        switch -Regex ($scope) {
            '^F(u|ull){0,1}' {
                $params["Full"] = $true
            }
            '^D(e|etail){0,1}' {
                $params["Detailed"] = $true
            }
            '^E(x|xample){0,1}' {
                $params["Examples"] = $true
            }
            '^O(n|nline){0,1}$' {
                $params["Online"] = $true
            }
        }
        return (Get-Help @params)
    }

    [string] Docs() {
        if ([string]::IsNullOrEmpty($this._awsDocumentation)) {
            return "AWS Documentation link not found for this class!"
        }
        Start-Process $this._awsDocumentation
        return "Opening documentation link: $($this._awsDocumentation)"
    }

    [System.Collections.Specialized.OrderedDictionary] ToOrderedDictionary() {
        return $this.ToOrderedDictionary($false)
    }

    [System.Collections.Specialized.OrderedDictionary] ToOrderedDictionary([bool] $addAllProperties) {
        $clean = [ordered]@{}
        $this.PSObject.Properties | ForEach-Object {
            $key = $_.Name
            $value = $_.Value
            if (
                $addAllProperties -or (
                    $key -notmatch '^(_|LogicalId$)' -and (
                        $value -is [enum] -or
                        $key -match '::' -or
                        $null -ne $value
                    ) -and (
                        $value -isnot [string] -or
                        -not [string]::IsNullOrEmpty($value)
                    )
                )
            ) {
                $clean[$key] = if ($key -match '$(UpdateReplacePolicy|DeletionPolicy)$' -and $value.ToString() -match '^(Delete|Retain|Snapshot)$') {
                    (Get-Culture).TextInfo.ToTitleCase($value.ToString().ToLower())
                }
                elseif ($value -is [enum]) {
                    $value.ToString()
                }
                elseif ($value -is [System.Collections.IDictionary] -and $value -isnot [VSHashtable]) {
                    $value
                }
                elseif ($value | Get-Member -Name ToOrderedDictionary* -MemberType Method -ErrorAction SilentlyContinue) {
                    try {
                        $value.ToOrderedDictionary($addAllProperties)
                    }
                    catch {
                        $value
                    }
                }
                else {
                    $value
                }
                Write-Debug "Key matched: $key"
                Write-Debug "Value matched: $($clean[$key])"
            }
            else {
                Write-Debug "Key excluded: $key"
            }
        }
        return $clean
    }

    [string] ToJson() {
        return $this.ToJson($false)
    }

    [string] ToJson([bool] $compress) {
        $clean = if ($this.PSObject.Properties.Name -contains 'LogicalId') {
            @{$this.LogicalId = $this.ToOrderedDictionary()}
        }
        else {
            $this.ToOrderedDictionary()
        }
        return $clean | ConvertTo-Json -Depth 50 -Compress:$compress
    }

    [string] ToYaml() {
        return $this.ToYaml($false)
    }

    [string] ToYaml([bool] $usePowerShellYaml) {
        $flipped = if (-not $usePowerShellYaml -and $null -ne (Get-Command cfn-flip* -ErrorAction SilentlyContinue)) {
            ($this.ToJson() | cfn-flip) -join [System.Environment]::NewLine
        }
        else {
            $clean = if ($this.PSObject.Properties.Name -contains 'LogicalId') {
                @{$this.LogicalId = $this.ToOrderedDictionary()}
            }
            else {
                $this.ToOrderedDictionary()
            }
            ($clean | ConvertTo-Yaml) -join [System.Environment]::NewLine
        }
        return $flipped
    }

    VSObject() {
        $this._addAccessors()
    }

    VSObject([IDictionary] $props) {
        $this._addAccessors()
        Write-Debug "[$($this.GetType())] Contructing from input IDictionary"
        $props.GetEnumerator() | ForEach-Object {
            Write-Debug "[$($this.GetType())] [$($_.Key)] Checking for property"
            if ($_.Key -eq 'Fn::Transform' -or ($this.PSObject.Properties.Name -contains $_.Key -and $_.Key -notin $this._commonParams)) {
                Write-Debug "[$($this.GetType())] [$($_.Key)] Property found, adding value: $($_.Value)"
                $val = if ($_.Value -is [enum]) {
                    $_.Value.ToString()
                }
                else {
                    $_.Value
                }
                $this."$($_.Key)" = $val
            }
        }
    }

    VSObject([psobject] $props) {
        $this._addAccessors()
        Write-Debug "Contructing $($this.GetType()) from input PSObject"
        $props.PSObject.Properties | ForEach-Object {
            Write-Debug "[$($this.GetType())] [$($_.Name)] Checking for property"
            if ($_.Name -eq 'Fn::Transform' -or ($this.PSObject.Properties.Name -contains $_.Name -and $_.Name -notin $this._commonParams)) {
                Write-Debug "[$($this.GetType())] [$($_.Name)] Property found, adding value: $($_.Value)"
                $val = if ($_.Value -is [enum]) {
                    $_.Value.ToString()
                }
                else {
                    $_.Value
                }
                $this."$($_.Name)" = $val
            }
        }
    }
}

Write-Verbose "Importing class 'ConditionFunction'"

class ConditionFunction : VSHashtable {
    hidden [string] $_topLevelKey = '_SHOULD_BE_OVERRIDDEN'
    hidden [type[]] $_validTypes = @([string], [int], [bool], [IDictionary], [psobject], [FnFindInMap], [FnRef], [ConditionFunction])

    [string] ToString() {
        return "$($this._topLevelKey -replace '\W' -replace '^Fn','Con')($($this[$this._topLevelKey]))"
    }

    hidden [void] _validateInput([object[]] $inputData) {}

    ConditionFunction() : base() {}
    ConditionFunction([object[]] $value) {
        $this._addAccessors()
        $this._validateInput($value)
        $this[$this._topLevelKey] = @()
        foreach ($item in $value) {
            $isValid = foreach ($type in $this._validTypes) {
                if ($item -is $type) {
                    $true
                    break
                }
            }
            if (-not $isValid) {
                throw [VSError]::InvalidType($item, $this._validTypes)
            }
            Write-Debug "Adding $($this.GetType().Name) from input type $($item.GetType())"
            $this[$this._topLevelKey] += $item
        }
    }
}

Write-Verbose "Importing class 'IntrinsicFunction'"

class IntrinsicFunction : VSHashtable {
    hidden [string] $_topLevelKey = '_SHOULD_BE_OVERRIDDEN'
    hidden [type[]] $_validTypes = @([string], [int], [IDictionary], [psobject], [IntrinsicFunction], [ConditionFunction])

    [string] ToString() {
        return "$($this._topLevelKey -replace '\W')($($this[$this._topLevelKey]))"
    }

    hidden [void] _validateInput([object] $inputData) {}

    IntrinsicFunction() : base() {}
    IntrinsicFunction([object] $value) {
        $this._addAccessors()
        $this._validateInput($value)
        $isValid = foreach ($type in $this._validTypes) {
            if ($value -is $type) {
                $true
                break
            }
        }
        if (-not $isValid) {
            throw [VSError]::InvalidType($value, $this._validTypes)
        }
        if ($value -is [IDictionary] -and $value -isnot [IntrinsicFunction] -and $value -isnot [ConditionFunction]) {
            if ($value.Contains($this._topLevelKey)) {
                Write-Debug "Contructing $($this.GetType().Name) from input IDictionary"
                $this[$this._topLevelKey] = $value[$this._topLevelKey]
            }
            else {
                throw [VSError]::InvalidArgument($value, "The input object is missing the Key '$($this._topLevelKey)'. Unable to construct a $($this.GetType()) object from the input data.")
            }
        }
        elseif ($value -is [psobject]) {
            if ($value.PSObject.Properties.Name -contains $this._topLevelKey) {
                Write-Debug "Contructing $($this.GetType().Name) from input PSObject"
                $this[$this._topLevelKey] = $value."$($this._topLevelKey)"
            }
            else {
                throw [VSError]::InvalidArgument($value, "The input object is missing the Property '$($this._topLevelKey)'. Unable to construct a $($this.GetType()) object from the input data.")
            }
        }
        else {
            $this[$this._topLevelKey] = $value
        }
    }
}

Write-Verbose "Importing class 'VSLogicalObject'"
class VSLogicalObject : VSObject {
    [ValidateLogicalId()] [string] $LogicalId

    [string] ToString() {
        return $this.LogicalId
    }

    VSLogicalObject() : base() {}
    VSLogicalObject([IDictionary] $props) : base($props) {}
    VSLogicalObject([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'VSJson'"

class VSJson : VSHashtable {
    static [string] Decode([string] $stringOrFilepath) {
        return [WebUtility]::UrlDecode($stringOrFilepath)
    }

    static [Specialized.OrderedDictionary] TransformToDict([string] $stringOrFilepath) {
        $dictionary = [ordered]@{}
        if (Test-Path $stringOrFilepath) {
            $stringOrFilepath = [File]::ReadAllText($stringOrFilepath)
        }
        if ($stringOrFilepath.Trim() -match '^%\d\w') {
            Write-Debug "String appears to be URL encoded, decoding"
            $stringOrFilepath = [VSJson]::Decode($stringOrFilepath)
        }
        try {
            $jsonConvert = @{
                InputObject = $stringOrFilepath
                ErrorAction = 'Stop'
            }
            if ($Global:PSVersionTable.PSVersion.Major -ge 7) {
                $jsonConvert['Depth'] = 20
            }
            $final = ConvertFrom-Json @jsonConvert
            $final.PSObject.Properties | ForEach-Object {
                Write-Debug "Adding key '$($_.Name)' with value '$($_.Value)' to VSJson"
                $dictionary[$_.Name] = $_.Value
            }
        }
        catch {
            throw [VSError]::InvalidJsonInput($stringOrFilepath)
        }
        Write-Debug "Resulting dict: $($dictionary | ConvertTo-Json -Depth 10))"
        return $dictionary
    }

    static [VSJson] Transform([string] $stringOrFilepath) {
        $vsJson = [VSJson]::new()
        $stringOrFilepath = [VSJson]::Decode($stringOrFilepath)
        $dictionary = [VSJson]::TransformToDict($stringOrFilepath)
        $dictionary.GetEnumerator() | ForEach-Object {
            $vsJson[$_.Key] = $_.Value
        }
        return $vsJson
    }

    static [VSJson] Transform([VSJson] $vsJson) {
        return $vsJson
    }

    VSJson() : base() {}
    VSJson([IDictionary] $dictionary) {
        $dictionary.GetEnumerator() | ForEach-Object {
            Write-Debug "Adding key '$($_.Key)' with value '$($_.Value)' to $this"
            $this[$_.Key] = $_.Value
        }
    }
    VSJson([psobject] $psObject) {
        $psObject.PSObject.Properties | ForEach-Object {
            Write-Debug "Adding property '$($_.Name)' with value '$($_.Value)' to $this"
            $this[$_.Name] = $_.Value
        }
    }
    VSJson([string] $stringOrFilepath) {
        $stringOrFilepath = [VSJson]::Decode($stringOrFilepath)
        $dictionary = [VSJson]::TransformToDict($stringOrFilepath)
        $dictionary.GetEnumerator() | ForEach-Object {
            Write-Debug "Adding key '$($_.Key)' with value '$($_.Value)' to $this"
            $this[$_.Key] = $_.Value
        }
    }
    VSJson([string[]] $strings) {
        $newString = $strings -join [Environment]::NewLine
        $stringOrFilepath = [VSJson]::Decode($newString)
        $dictionary = [VSJson]::TransformToDict($stringOrFilepath)
        $dictionary.GetEnumerator() | ForEach-Object {
            Write-Debug "Adding key '$($_.Key)' with value '$($_.Value)' to $this"
            $this[$_.Key] = $_.Value
        }
    }
}

Write-Verbose "Importing class 'VSTimestamp'"
class VSTimestamp : VSObject {
    hidden [string] $_timestamp

    static [string] Help() {
        return "Help content has not been created for this class. Please open an issue on the GitHub repository to request help for this class."
    }

    static [string] Transform([datetime] $dateTime) {
        return $dateTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')
    }

    static [string] Transform([string] $dateString) {
        return (Get-Date $dateString).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')
    }

    [string] ToString() {
        return $this._timestamp
    }

    VSTimestamp() {}

    VSTimestamp([datetime] $dateTime) {
        $this._timestamp = $this::Transform($dateTime)
    }

    VSTimestamp([string] $dateString) {
        $this._timestamp = $this::Transform($dateString)
    }
}

Write-Verbose "Importing class 'VSYaml'"

class VSYaml : VSJson {
    static [Specialized.OrderedDictionary] TransformToDict([string] $stringOrFilepath) {
        $dictionary = [ordered]@{}
        if (Test-Path $stringOrFilepath) {
            $stringOrFilepath = [File]::ReadAllText($stringOrFilepath)
        }
        if ($stringOrFilepath.Trim() -match '^%\d\w') {
            Write-Debug "String appears to be URL encoded, decoding"
            $stringOrFilepath = [VSYaml]::Decode($stringOrFilepath)
        }
        try {
            $final = if (Get-Command cfn-flip* -ErrorAction SilentlyContinue) {
                $json = $stringOrFilepath | cfn-flip | Out-String

                $jsonConvert = @{
                    InputObject = $json
                    ErrorAction = 'Stop'
                }
                if ($Global:PSVersionTable.PSVersion.Major -ge 7) {
                    $jsonConvert['Depth'] = 20
                }
                $final = ConvertFrom-Json @jsonConvert
            }
            else {
                ConvertFrom-Yaml -Yaml $stringOrFilepath -ErrorAction Stop
            }
        }
        catch {
            throw [VSError]::InvalidYamlInput($stringOrFilepath)
        }
        $final.PSObject.Properties | ForEach-Object {
            Write-Debug "Adding key '$($_.Name)' with value '$($_.Value)' to VSYaml"
            $dictionary[$_.Name] = $_.Value
        }
        Write-Debug "Resulting dict: $($dictionary | ConvertTo-Json -Depth 10))"
        return $dictionary
    }

    static [VSYaml] Transform([string] $stringOrFilepath) {
        $vsYaml = [VSYaml]::new()
        $stringOrFilepath = [VSYaml]::Decode($stringOrFilepath)
        $dictionary = [VSYaml]::TransformToDict($stringOrFilepath)
        $dictionary.GetEnumerator() | ForEach-Object {
            $vsYaml[$_.Key] = $_.Value
        }
        return $vsYaml
    }

    static [VSYaml] Transform([VSYaml] $vsYaml) {
        return $vsYaml
    }

    VSYaml() : base() {}
    VSYaml([IDictionary] $dictionary) : base($dictionary) {}
    VSYaml([psobject] $psObject) : base($psObject) {}
    VSYaml([string] $stringOrFilepath) : base($stringOrFilepath) {}
    VSYaml([string[]] $strings) : base($strings) {}
} #>

Write-Verbose "Importing class 'VSTag'"

class VSTag : VSHashtable {
    hidden [string] $_vsFunctionName = 'Add-VSTag'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html'

    [string] $Key
    [object] $Value

    static [VSTag[]] TransformTag([object] $inputData) {
        $final = [List[VSTag]]::new()
        $list = if ($inputData -is [array]) {
            $inputData
        }
        else {
            @($inputData)
        }
        foreach ($item in $list) {
            if ($item -is [VSTag]) {
                $final.Add($item)
            }
            elseif ($item -is [IDictionary]) {
                if ($item['Key'] -and $item['Value']) {
                    $final.Add(
                        [VSTag]@{
                            Key   = $item['Key']
                            Value = $item['Value']
                        }
                    )
                }
                else {
                    $item.GetEnumerator() | ForEach-Object {
                        $final.Add(
                            [VSTag]@{
                                Key   = $_.Key
                                Value = $_.Value
                            }
                        )
                    }
                }
            }
            elseif ($item -is [psobject]) {
                if ($item.PSObject.Properties.Name -contains 'Key' -and $item.PSObject.Properties.Name -contains 'Value') {
                    $final.Add(
                        [VSTag]@{
                            Key   = $item.Key
                            Value = $item.Value
                        }
                    )
                }
                else {
                    $item.PSObject.Properties | ForEach-Object {
                        $final.Add(
                            [VSTag]@{
                                Key   = $_.Name
                                Value = $_.Value
                            }
                        )
                    }
                }
            }
        }
        return $final
    }

    VSTag() {}

    VSTag([IDictionary] $inputData) {
        $this.Key = $inputData.Key
        $this.Value = $inputData.Value
    }

    VSTag([psobject] $inputData) {
        $this.Key = $inputData.Key
        $this.Value = $inputData.Value
    }

    VSTag([object] $key, [object] $value) {
        $this.Key = $key.ToString()
        $this.Value = $value
    }
}

Write-Verbose "Importing class 'FnBase64'"

class FnBase64 : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnBase64'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html'
    hidden [string] $_topLevelKey = 'Fn::Base64'

    FnBase64() : base() {}
    FnBase64([object] $value) : base($value) {}
}

Write-Verbose "Importing class 'FnCidr'"

class FnCidr : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnCidr'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html'
    hidden [string] $_topLevelKey = 'Fn::Cidr'

    hidden [void] _validateInput([object] $inputData) {
        if ($inputData.Count -ne 3) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
    }

    FnCidr() : base() {}
    FnCidr([object] $value) : base($value) {}

    FnCidr(
        [object] $ipBlock,
        [object] $count,
        [object] $cidrBits
    ) {
        foreach ($item in @($ipBlock,$count,$cidrBits)) {
            $isValid = foreach ($type in $this._validTypes) {
                if ($item -is $type) {
                    $true
                    break
                }
            }
            if (-not $isValid) {
                throw [VSError]::InvalidType($item, $this._validTypes)
            }
        }
        $this[$this._topLevelKey] = @($ipBlock, $count, $cidrBits)
    }
}

Write-Verbose "Importing class 'FnFindInMap'"

class FnFindInMap : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnFindInMap'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html'
    hidden [string] $_topLevelKey = 'Fn::FindInMap'

    hidden [void] _validateInput([object] $inputData) {
        if ($inputData.Count -ne 3) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
        elseif ($inputData[0] -isnot [string]) {
            throw [VSError]::InvalidArgument($inputData, "The first item provided when constructing a <$($this.GetType())> object needs to be a string. Type provided: $($inputData[0].GetType())")
        }
    }

    FnFindInMap() : base() {}
    FnFindInMap([object] $value) : base($value) {}

    FnFindInMap(
        [string] $mapName,
        [object] $topLevelKey,
        [object] $secondLevelKey
    ) {
        foreach ($item in @($topLevelKey,$secondLevelKey)) {
            $isValid = foreach ($type in $this._validTypes) {
                if ($item -is $type) {
                    $true
                    break
                }
            }
            if (-not $isValid) {
                throw [VSError]::InvalidType($item, $this._validTypes)
            }
        }
        $this[$this._topLevelKey] = @($mapName, $topLevelKey, $secondLevelKey)
    }
}

Write-Verbose "Importing class 'FnGetAtt'"
class FnGetAtt : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnGetAtt'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html'
    hidden [string] $_topLevelKey = 'Fn::GetAtt'

    hidden [void] _validateInput([object] $inputData) {
        if ($inputData.Count -ne 2) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
    }

    FnGetAtt() : base() {}
    FnGetAtt([object] $value) : base($value) {}

    FnGetAtt([string] $logicalNameOfResource, [string] $attributeName) {
        $this[$this._topLevelKey] = @($logicalNameOfResource,$attributeName)
    }
}

Write-Verbose "Importing class 'FnGetAZs'"

class FnGetAZs : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnGetAZs'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html'
    hidden [string] $_topLevelKey = 'Fn::GetAZs'

    FnGetAZs() { $this[$this._topLevelKey] = '' }
    FnGetAZs([object] $value) : base($value) {}
}

Write-Verbose "Importing class 'FnImportValue'"

class FnImportValue : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnImportValue'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html'
    hidden [string] $_topLevelKey = 'Fn::ImportValue'

    FnImportValue() : base() {}
    FnImportValue([object] $value) : base($value) {}
}

Write-Verbose "Importing class 'FnJoin'"
class FnJoin : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnJoin'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html'
    hidden [string] $_topLevelKey = 'Fn::Join'

    hidden [void] _validateInput([object] $inputData) {
        if ($inputData.Count -ne 2) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
        elseif ($inputData[0] -isnot [string]) {
            throw [VSError]::InvalidArgument($inputData, "The first item provided when constructing a <$($this.GetType())> object needs to be a string. Type provided: $($inputData[0].GetType())")
        }
    }

    FnJoin() : base() {}
    FnJoin([object] $value) : base($value) {}

    FnJoin(
        [string] $delimiter,
        [object[]] $listOfValues
    ) {
        $validTypes = @([string], [int], [IntrinsicFunction], [ConditionFunction])
        foreach ($value in $listOfValues) {
            $isValid = foreach ($type in $validTypes) {
                if ($value -is $type) {
                    $true
                    break
                }
            }
            if (-not $isValid) {
                throw [VSError]::InvalidType($value, $validTypes)
            }
        }
        $this[$this._topLevelKey] = @($delimiter, @($listOfValues))
    }
}

Write-Verbose "Importing class 'FnRef'"
class FnRef : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnRef'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html'

    static [FnRef] $AccountId = [FnRef]::new('AWS::AccountId')
    static [FnRef] $Include = [FnRef]::new('AWS::Include')
    static [FnRef] $NotificationARNs = [FnRef]::new('AWS::NotificationARNs')
    static [FnRef] $NoValue = [FnRef]::new('AWS::NoValue')
    static [FnRef] $Partition = [FnRef]::new('AWS::Partition')
    static [FnRef] $Region = [FnRef]::new('AWS::Region')
    static [FnRef] $StackId = [FnRef]::new('AWS::StackId')
    static [FnRef] $StackName = [FnRef]::new('AWS::StackName')
    static [FnRef] $URLSuffix = [FnRef]::new('AWS::URLSuffix')

    [string] $Ref

    [string] ToString() {
        return "Ref($($this['Ref']))"
    }

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name Ref -Value {
            $this['Ref']
        } -SecondValue {
            param([string] $ref)
            $this['Ref'] = $ref
        }
    }

    FnRef() {}

    FnRef([string] $ref) {
        $this['Ref'] = $ref
    }

    FnRef([VSLogicalObject] $vsLogicalObject) {
        $this['Ref'] = $vsLogicalObject.ToString()
    }
}

Write-Verbose "Importing class 'FnSelect'"
class FnSelect : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnSelect'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-select.html'
    hidden [string] $_topLevelKey = 'Fn::Select'

    hidden [void] _validateInput([object] $inputData) {
        if ($inputData.Count -ne 2) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
    }

    FnSelect() : base() {}
    FnSelect([object] $value) : base($value) {}

    FnSelect(
        [object] $index,
        [object[]] $listOfObjects
    ) {
        $isValid = foreach ($type in $this._validTypes) {
            if ($index -is $type) {
                $true
                break
            }
        }
        if (-not $isValid) {
            throw [VSError]::InvalidType($index, $this._validTypes)
        }
        foreach ($value in $listOfObjects) {
            $isValid = foreach ($type in $this._validTypes) {
                if ($value -is $type) {
                    $true
                    break
                }
            }
            if (-not $isValid) {
                throw [VSError]::InvalidType($value, $this._validTypes)
            }
        }
        $this[$this._topLevelKey] = @($index, @($listOfObjects))
    }
}

Write-Verbose "Importing class 'FnSplit'"

class FnSplit : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnSplit'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-split.html'
    hidden [string] $_topLevelKey = 'Fn::Split'

    hidden [void] _validateInput([object] $inputData) {
        if ($inputData.Count -ne 2) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
        elseif ($inputData[0] -isnot [string]) {
            throw [VSError]::InvalidArgument($inputData, "The first item provided when constructing a <$($this.GetType())> object needs to be a string. Type provided: $($inputData[0].GetType())")
        }
    }

    FnSplit() : base() {}
    FnSplit([object] $value) : base($value) {}

    FnSplit(
        [string] $delimiter,
        [object] $sourceString
    ) {
        $isValid = foreach ($type in $this._validTypes) {
            if ($sourceString -is $type) {
                $true
                break
            }
        }
        if (-not $isValid) {
            throw [VSError]::InvalidType($sourceString, $this._validTypes)
        }
        $this[$this._topLevelKey] = @($delimiter,$sourceString)
    }
}

Write-Verbose "Importing class 'FnSub'"

class FnSub : IntrinsicFunction {
    hidden [string] $_vsFunctionName = 'Add-FnSub'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html'
    hidden [string] $_topLevelKey = 'Fn::Sub'

    FnSub() {}
    FnSub([string] $string) {
        $this['Fn::Sub'] = $String
    }

    FnSub(
        [string] $string,
        [IDictionary] $mapping
    ) {
        $this['Fn::Sub'] = @($String,$Mapping)
    }
}

Write-Verbose "Importing class 'FnTransform'"


class FnTransform : VSHashtable {
    hidden [string] $_vsFunctionName = 'Add-FnTransform'
    hidden [string] $_awsDocumentation = 'http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-transform.html'

    [string] $LogicalId = 'Fn::Transform'
    [string] $Name
    [IDictionary] $Parameters

    [string] ToString() {
        return "FnTransform($($this['Fn::Transform']))"
    }

    hidden [void] _addAccessors() {
        $this.LogicalId = 'Fn::Transform'
        $this['Name'] = ''
        $this | Add-Member -Force -MemberType ScriptProperty -Name 'LogicalId' -Value {
            'Fn::Transform'
        } -SecondValue {
            param([object] $value)
            $this.LogicalId = 'Fn::Transform'
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name 'Parameters' -Value {
            $this['Parameters']
        } -SecondValue {
            param([object] $value)
            if ($value -is [IDictionary]) {
                $this['Parameters'] = $value
            }
            elseif ($value -is [psobject]) {
                $ord = [ordered]@{}
                $value.PSObject.Properties | ForEach-Object {
                    $ord[$_.Name] = $_.Value
                }
                $this['Parameters'] = $ord
            }
            else {
                throw [VSError]::InvalidArgument($value,"Input value for the Parameters property on an FnTransform object must be either an IDictionary or a PSObject.")
            }
        }
    }

    FnTransform() : base() {}
    FnTransform([IDictionary] $props) : base($props) {}
    FnTransform([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'Include'"

class Include : FnTransform {
    hidden [string] $_vsFunctionName = 'Add-Include'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/create-reusable-transform-function-snippets-and-add-to-your-template-with-aws-include-transform.html'

    [string] ToString() {
        return "Include($($this.Parameters['Location']))"
    }

    hidden [void] SetLocation([object] $inputData) {
        $this['LogicalId'] = 'Fn::Transform'
        $this['Name'] = 'AWS::Include'
        if ($null -eq $this['Parameters']) {
            $this['Parameters'] = [ordered]@{Location = ''}
        }
        $props = if ($inputData -is [string]) {
            [pscustomobject]@{
                Location = $inputData
            }
        }
        elseif ($inputData -is [IDictionary]) {
            [pscustomobject]$inputData
        }
        elseif ($inputData -is [psobject]) {
            $inputData
        }
        else {
            $errorRecord = [VSError]::new(
                [ArgumentException]::new("Invalid input! Please either pass an IDictionary or PSObject containing a Parameters or Location property or just the S3 location as a string value."),
                'InvalidInput',
                [ErrorCategory]::InvalidArgument,
                $inputData
            )
            throw [VSError]::InsertError($errorRecord)
        }
        if ($props.PSObject.Properties.Name -contains 'Parameters') {
            if ($props.Parameters.Location -notmatch '^s3:\/\/.*') {
                $errorRecord = [VSError]::new(
                    [ArgumentException]::new("$($props.Parameters.Location) is not a valid s3 path! Location must match pattern '^s3:\/\/.*'"),
                    'InvalidLocation',
                    [ErrorCategory]::InvalidArgument,
                    $props
                )
                throw [VSError]::InsertError($errorRecord)
            }
            else {
                $this['Parameters']['Location'] = $props.Parameters.Location
            }
        }
        elseif ($props.PSObject.Properties.Name -contains 'Location') {
            if ($props.Location -match '^s3:\/\/.*') {
                $this['Parameters']['Location'] = $props.Location
            }
            else {
                $errorRecord = [VSError]::new(
                    [ArgumentException]::new("$($props.Location) is not a valid s3 path! Location must match pattern '^s3:\/\/.*'"),
                    'InvalidLocation',
                    [ErrorCategory]::InvalidArgument,
                    $props
                )
                throw [VSError]::InsertError($errorRecord)
            }
        }
        else {
            $errorRecord = [VSError]::new(
                [ArgumentException]::new("Invalid input! Please either pass an IDictionary or PSObject containing a Parameters or Location property or just the S3 location as a string value."),
                'InvalidInput',
                [ErrorCategory]::InvalidArgument,
                $props
            )
            throw [VSError]::InsertError($errorRecord)
        }
    }

    Include() {}

    Include([IDictionary] $props) {
        $this.SetLocation($props)
    }

    Include([psobject] $props) {
        $this.SetLocation($props)
    }

    Include([string] $location) {
        $this.SetLocation($location)
    }
}

Write-Verbose "Importing class 'VSTemplate'"

class VSTemplate : VSObject {
    hidden [string] $_vsFunctionName = 'Initialize-VaporShell'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html'

    hidden [string]$_description = $null
    hidden [string] $_awsTemplateFormatVersion = $null
    hidden [OrderedDictionary] $_mappings = $null
    hidden [object[]] $_mappingsOriginal = @()
    hidden [OrderedDictionary] $_parameters = $null
    hidden [object[]] $_parametersOriginal = @()
    hidden [OrderedDictionary] $_resources = $null
    hidden [object[]] $_resourcesOriginal = @()
    hidden [OrderedDictionary] $_outputs = $null
    hidden [object[]] $_outputsOriginal = @()
    hidden [OrderedDictionary] $_metadata = $null
    hidden [object[]] $_metadataOriginal = @()
    hidden [object[]] $_transform = @()
    hidden [OrderedDictionary] $_conditions = $null
    hidden [object[]] $_conditionsOriginal = @()

    [string] $AWSTemplateFormatVersion = $null
    [string] $Description = $null
    [FnTransform[]] $Transform = $null
    [VSParameter[]] $Parameters = $null
    [VSCondition[]] $Conditions = $null
    [VSMetadata[]] $Metadata = $null
    [VSMapping[]] $Mappings = $null
    [VSResource[]] $Resources = $null
    [VSOutput[]] $Outputs = $null

    static [string] Help() {
        $help = "This is the Template help."
        return $help
    }

    [string] ToString() {
        return $this.ToJson()
    }

    [string] Export([bool] $passThru, [string] $format) {
        if ($null -eq $this.Resources) {
            throw [VSError]::InvalidArgument($this,"Unable to find any resources on this Vaporshell template. Resources are required in CloudFormation templates at the minimum.")
        }
        $out = switch -RegEx ($format.ToLower()) {
            '^(y|yml|yaml)$' {
                $this.ToYaml()
            }
            '^(j|jsn|json)$' {
                $this.ToJson()
            }
            default {
                $this.ToJson()
            }
        }
        return $out
    }

    [void] Export ([string] $path) {
        $format = switch -RegEx ($Path) {
            '\.(yml|yaml)$' {
                'YAML'
            }
            default {
                'JSON'
            }
        }
        $this.Export($path, $format, $false)
    }

    [void] Export ([string] $path, [bool] $force) {
        $format = switch -RegEx ($Path) {
            '\.(yml|yaml)$' {
                'YAML'
            }
            default {
                'JSON'
            }
        }
        $this.Export($path, $format, $force)
    }

    [void] Export([string] $path, [string] $format, [bool] $force) {
        if ($null -eq $this.Resources) {
            throw [VSError]::InvalidArgument($this,"Unable to find any resources on this Vaporshell template. Resources are required in CloudFormation templates at the minimum.")
        }
        $forcePref = @{}
        if ($force) {
            $forcePref.add("Force",$True)
        }
        switch -RegEx ($format.ToLower()) {
            '^(yml|yaml)$' {
                $this.ToYaml() | Set-Content $path @forcePref
            }
            '^(template|json|cfn|cf)$' {
                $this.ToJson() | Set-Content $path @forcePref
            }
        }
    }

    [void] Remove([string] $logicalId, [string] $section) {
        $validSections = @('Parameters', 'Conditions', 'Metadata', 'Mappings', 'Resources', 'Outputs','Globals')
        if ($this.PSObject.Properties.Name -notcontains $section) {
            $message = "The section $section was not found on $($this.GetType()). Valid sections: $($validSections -join ', ')"
            throw [VSError]::InvalidArgument($logicalId, $message)
        }
        $_section = '_' + $section.Substring(0, 1).ToLower() + $section.Substring(1)
        $_sectionOriginal = $_section + 'Original'
        if (($this.$_section).Contains($logicalId)) {
            ($this.$_section).Remove($logicalId) | Out-Null
            $this.$_sectionOriginal = $this.$_sectionOriginal | Where-Object { $_.LogicalId -ne $logicalId }
        }
    }

    [void] RemoveParameter([string] $logicalId) {
        $this.Remove($logicalId, 'Parameters')
    }

    [void] RemoveCondition([string] $logicalId) {
        $this.Remove($logicalId, 'Conditions')
    }

    [void] RemoveMetadata([string] $logicalId) {
        $this.Remove($logicalId, 'Metadata')
    }

    [void] RemoveMapping([string] $logicalId) {
        $this.Remove($logicalId, 'Mappings')
    }

    [void] RemoveResource([string] $logicalId) {
        $this.Remove($logicalId, 'Resources')
    }

    [void] RemoveOutput([string] $logicalId) {
        $this.Remove($logicalId, 'Outputs')
    }

    [void] AddTransform([object] $transform) {
        if ($transform -is [string]) {
            if ($transform -match 'Serverless' -and $this._transform -notcontains 'AWS::Serverless-2016-10-31') {
                $this._transform += 'AWS::Serverless-2016-10-31'
            }
            else {
                $this._transform += $transform
            }
        }
        elseif ($transform -is [FnTransform]) {
            $this._transform += $transform.ToOrderedDictionary()
        }
        elseif ($cast = $transform -as [FnTransform]) {
            $this._transform += $cast.ToOrderedDictionary()
        }
        else {
            throw [VSError]::InvalidType($transform, @([string], [FnTransform]))
        }
    }

    [void] AddTransform([object[]] $transforms) {
        $transforms | ForEach-Object {
            $this.AddTransform($_)
        }
    }

    [void] AddSAMTransform() {
        $this.AddTransform('AWS::Serverless-2016-10-31')
    }

    [void] AddCondition([object] $item) {
        if ($item.GetType() -in @([string],[int],[bool],[double],[long])) {
            throw [VSError]::InvalidType($item,@([VSObject],[VSHashtable],[psobject],[IDictionary]))
        }
        elseif ($null -eq $this._conditions) {
            $this._conditions = [ordered]@{}
        }
        if ($null -eq $item.LogicalId) {
            throw [VSError]::MissingLogicalId($item, 'Condition')
        }
        elseif ($item -is [VSCondition] -and $this._conditions.Contains($item.LogicalId)) {
            throw [VSError]::DuplicateLogicalId($item, 'Condition')
        }
        elseif ($item -is [VSCondition]) {
            $this._conditions[$item.LogicalId] = $item.Condition
        }
        elseif ($item -is [FnTransform]) {
            $cleaned = [ordered]@{
                Name = $item.Name
                Parameters = $item.Parameters
            }
            $this._conditions[$item.LogicalId] = $cleaned
        }
        elseif ($cast = $item -as [FnTransform]) {
            $cleaned = [ordered]@{
                Name = $cast.Name
                Parameters = $cast.Parameters
            }
            $this._conditions[$cast.LogicalId] = $cleaned
        }
        else {
            throw [VSError]::InvalidType($item, @([VSCondition], [FnTransform]))
        }
        $this._conditionsOriginal += $item
    }

    [void] AddCondition([object[]] $items) {
        $items | ForEach-Object {
            $this.AddCondition($_)
        }
    }

    [void] AddMapping([object] $item) {
        if ($item.GetType() -in @([string],[int],[bool],[double],[long])) {
            throw [VSError]::InvalidType($item,@([VSObject],[VSHashtable],[psobject],[IDictionary]))
        }
        elseif ($null -eq $this._mappings) {
            $this._mappings = [ordered]@{}
        }
        if ($null -eq $item.LogicalId) {
            throw [VSError]::MissingLogicalId($item, 'Mapping')
        }
        elseif ($item -is [VSMapping] -and $this._mappings.Contains($item.LogicalId)) {
            throw [VSError]::DuplicateLogicalId($item, 'Mapping')
        }
        elseif ($item -is [VSMapping]) {
            $this._mappings[$item.LogicalId] = $item.Map
        }
        elseif ($item -is [FnTransform]) {
            if ($this._mappings.Contains($item.LogicalId)) {
                $this._mappings[$item.LogicalId] += $item.ToOrderedDictionary()
            }
            else {
                $this._mappings[$item.LogicalId] = $item.ToOrderedDictionary()
            }
        }
        elseif ($cast = $item -as [FnTransform]) {
            if ($this._mappings.Contains($item.LogicalId)) {
                $this._mappings[$item.LogicalId] += $cast.ToOrderedDictionary()
            }
            else {
                $this._mappings[$item.LogicalId] = $cast.ToOrderedDictionary()
            }
        }
        else {
            throw [VSError]::InvalidType($item, @([VSMapping], [FnTransform]))
        }
        $this._mappingsOriginal += $item
    }

    [void] AddMapping([object[]] $items) {
        $items | ForEach-Object {
            $this.AddMapping($_)
        }
    }

    [void] AddOutput([object] $item) {
        if ($item.GetType() -in @([string],[int],[bool],[double],[long])) {
            throw [VSError]::InvalidType($item,@([VSObject],[VSHashtable],[psobject],[IDictionary]))
        }
        elseif ($null -eq $this._outputs) {
            $this._outputs = [ordered]@{}
        }
        if ($null -eq $item.LogicalId) {
            throw [VSError]::MissingLogicalId($item, 'Output')
        }
        elseif ($item -is [VSOutput] -and $this._outputs.Contains($item.LogicalId)) {
            throw [VSError]::DuplicateLogicalId($item, 'Output')
        }
        elseif ($item -is [VSOutput]) {
            $cleaned = [ordered]@{}
            $safeList = [VSOutput]::new().PSObject.Properties.Name
            $item.ToOrderedDictionary().GetEnumerator() | ForEach-Object {
                if ($_.Key -in $safeList) {
                    $cleaned[$_.Key] = $_.Value
                }
            }
            $this._outputs[$item.LogicalId] = $cleaned
        }
        elseif ($item -is [FnTransform]) {
            if ($this._outputs.Contains($item.LogicalId)) {
                $this._outputs[$item.LogicalId] += $item.ToOrderedDictionary()
            }
            else {
                $this._outputs[$item.LogicalId] = $item.ToOrderedDictionary()
            }
        }
        elseif ($cast = $item -as [FnTransform]) {
            if ($this._outputs.Contains($item.LogicalId)) {
                $this._outputs[$item.LogicalId] += $cast.ToOrderedDictionary()
            }
            else {
                $this._outputs[$item.LogicalId] = $cast.ToOrderedDictionary()
            }
        }
        else {
            throw [VSError]::InvalidType($item, @([VSOutput], [FnTransform]))
        }
        $this._outputsOriginal += $item
    }

    [void] AddOutput([object[]] $items) {
        $items | ForEach-Object {
            $this.AddOutput($_)
        }
    }

    [void] AddParameter([object] $item) {
        if ($item.GetType() -in @([string],[int],[bool],[double],[long])) {
            throw [VSError]::InvalidType($item,@([VSObject],[VSHashtable],[psobject],[IDictionary]))
        }
        elseif ($null -eq $this._parameters) {
            $this._parameters = [ordered]@{}
        }
        if ($null -eq $item.LogicalId) {
            throw [VSError]::MissingLogicalId($item, 'Parameter')
        }
        elseif ($this._parameters.Contains($item.LogicalId)) {
            throw [VSError]::DuplicateLogicalId($item, 'Parameter')
        }
        else {
            $cleaned = [ordered]@{}
            $safeList = [VSParameter]::new().PSObject.Properties.Name
            $item.ToOrderedDictionary().GetEnumerator() | ForEach-Object {
                if ($_.Key -in $safeList) {
                    $cleaned[$_.Key] = $_.Value
                }
            }
            $this._parameters[$item.LogicalId] = $cleaned
            $this._parametersOriginal += $item
        }
    }

    [void] AddParameter([object[]] $items) {
        $items | ForEach-Object {
            $this.AddParameter($_)
        }
    }

    [void] AddMetadata([object] $item) {
        if ($item.GetType() -in @([string],[int],[bool],[double],[long])) {
            throw [VSError]::InvalidType($item,@([VSObject],[VSHashtable],[psobject],[IDictionary]))
        }
        elseif ($null -eq $this._metadata) {
            $this._metadata = [ordered]@{}
        }
        if ($null -eq $item.LogicalId) {
            throw [VSError]::MissingLogicalId($item, 'Metadata')
        }
        elseif ($item -is [VSMetadata] -and $this._metadata.Contains($item.LogicalId)) {
            throw [VSError]::DuplicateLogicalId($item, 'Metadata')
        }
        elseif ($item -is [VSMetadata]) {
            $this._metadata[$item.LogicalId] = $item.Metadata
        }
        elseif ($item -is [FnTransform]) {
            $cleaned = [ordered]@{
                Name = $item.Name
                Parameters = $item.Parameters
            }
            $this._metadata[$item.LogicalId] = $cleaned
        }
        elseif ($cast = $item -as [FnTransform]) {
            $cleaned = [ordered]@{
                Name = $cast.Name
                Parameters = $cast.Parameters
            }
            $this._metadata[$cast.LogicalId] = $cleaned
        }
        else {
            throw [VSError]::InvalidType($item, @([VSMetadata], [FnTransform]))
        }
        $this._metadataOriginal += $item
    }

    [void] AddMetadata([object[]] $items) {
        $items | ForEach-Object {
            $this.AddParameter($_)
        }
    }

    [void] AddResource([object] $item) {
        if ($item.GetType() -in @([string],[int],[bool],[double],[long])) {
            throw [VSError]::InvalidType($item,@([VSObject],[VSHashtable],[psobject],[IDictionary]))
        }
        elseif ($null -eq $this._resources) {
            $this._resources = [ordered]@{}
        }
        if ($null -eq $item.LogicalId) {
            throw [VSError]::MissingLogicalId($item, 'Resource')
        }
        elseif ($item -is [VSResource] -and $this._resources.Contains($item.LogicalId)) {
            throw [VSError]::DuplicateLogicalId($item, 'Resource')
        }
        elseif ($item -is [VSResource]) {
            $cleaned = [ordered]@{}
            $safeList = [VSResource]::new().PSObject.Properties.Name
            $item.ToOrderedDictionary().GetEnumerator() | ForEach-Object {
                if ($item.LogicalId -eq 'Fn::Transform' -or $_.Key -in $safeList) {
                    $cleaned[$_.Key] = $_.Value
                }
            }
            $this._resources[$item.LogicalId] = $cleaned
            if ($item.Type -match 'Serverless') {
                $this.AddTransform('Serverless')
            }
        }
        elseif ($item -is [FnTransform]) {
            if ($this._resources.Contains($item.LogicalId)) {
                $this._resources[$item.LogicalId] += $item.ToOrderedDictionary()
            }
            else {
                $this._resources[$item.LogicalId] = $item.ToOrderedDictionary()
            }
        }
        elseif ($cast = $item -as [FnTransform]) {
            if ($this._resources.Contains($item.LogicalId)) {
                $this._resources[$item.LogicalId] += $cast.ToOrderedDictionary()
            }
            else {
                $this._resources[$item.LogicalId] = $cast.ToOrderedDictionary()
            }
        }
        else {
            throw [VSError]::InvalidType($item, @([VSResource], [FnTransform]))
        }
        $this._resourcesOriginal += $item
    }

    [void] AddResource([object[]] $items) {
        $items | ForEach-Object {
            $this.AddResource($_)
        }
    }

    hidden [void] _addExtraAccessors() {}

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'AWSTemplateFormatVersion' -Value {
            $this._awsTemplateFormatVersion
        } -SecondValue {
            param([object] $value)
            if ($value -is [string]) {
                $this._awsTemplateFormatVersion = $value
            }
            elseif ($value -is [datetime]) {
                $this._awsTemplateFormatVersion = $value.ToString('yyyy-MM-dd')
            }
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Description' -Value {
            $this._description
        } -SecondValue {
            param([string] $value)
            $this._description = $value
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Transform' -Value {
            $this._transform
        } -SecondValue {
            param([object] $value)
            $this.AddTransform($value)
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Parameters' -Value {
            if ($MyInvocation.Line -match '\.Parameters') {
                $this._parametersOriginal
            }
            else {
                $this._parameters
            }
        } -SecondValue {
            param([object[]] $value)
            if ($null -eq $this._parameters) {
                $this._parameters = [ordered]@{}
            }
            $this.AddParameter($value)
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Conditions' -Value {
            if ($MyInvocation.Line -match '\.Conditions') {
                $this._conditionsOriginal
            }
            else {
                $this._conditions
            }
        } -SecondValue {
            param([object[]] $value)
            if ($null -eq $this._conditions) {
                $this._conditions = [ordered]@{}
            }
            $this.AddCondition($value)
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Metadata' -Value {
            if ($MyInvocation.Line -match '\.Metadata') {
                $this._metadataOriginal
            }
            else {
                $this._metadata
            }
        } -SecondValue {
            param([object[]] $value)
            if ($null -eq $this._metadata) {
                $this._metadata = [ordered]@{}
            }
            $this.AddMetadata($value)
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Mappings' -Value {
            if ($MyInvocation.Line -match '\.Mappings') {
                $this._mappingsOriginal
            }
            else {
                $this._mappings
            }
        } -SecondValue {
            param([object[]] $value)
            if ($null -eq $this._mappings) {
                $this._mappings = [ordered]@{}
            }
            $this.AddMapping($value)
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Resources' -Value {
            if ($MyInvocation.Line -match '\.Resources') {
                $this._resourcesOriginal
            }
            else {
                $this._resources
            }
        } -SecondValue {
            param([object[]] $value)
            if ($null -eq $this._resources) {
                $this._resources = [ordered]@{}
            }
            $this.AddResource($value)
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Outputs' -Value {
            if ($MyInvocation.Line -match '\.Outputs') {
                $this._outputsOriginal
            }
            else {
                $this._outputs
            }
        } -SecondValue {
            param([object[]] $value)
            if ($null -eq $this._outputs) {
                $this._outputs = [ordered]@{}
            }
            $this.AddOutput($value)
        }
        $this._addExtraAccessors()
    }

    VSTemplate() : base() {}
    VSTemplate([IDictionary] $props) : base($props) {}
    VSTemplate([psobject] $props) : base($props) {}
    VSTemplate([string] $pathOrBodyOrUrl) {
        Write-Debug "Building template from $pathOrBodyOrUrl"
        $templateBody = if (Test-Path $pathOrBodyOrUrl) {
            [System.IO.File]::ReadAllText((Resolve-Path $pathOrBodyOrUrl))
        } elseif ($pathOrBodyOrUrl -as [Uri]) {
            (Invoke-WebRequest -Uri $pathOrBodyOrUrl).Content
        } else {
            $pathOrBodyOrUrl
        }
        $baseObj = if ($templateBody -match "Resources:") {
            if (Get-Command cfn-flip -ErrorAction SilentlyContinue) {
                ConvertFrom-Json -InputObject (($templateBody | cfn-flip) -join [Environment]::NewLine)
            } else {
                $ht = ConvertFrom-Yaml -Yaml $templateBody -Ordered
                [PSCustomObject]$ht
            }
        } else {
            ConvertFrom-Json -InputObject $templateBody
        }
        #<#
        foreach ($section in $baseObj.PSObject.Properties) {
            Write-Debug "Importing section: $($section.Name)"
            switch -Regex ($section.Name) {
                '(Outputs|Parameters|Resources|Metadata|Mappings|Conditions|Globals)' {
                    $this."$($section.Name)" = @()
                    $sectionContents = $baseObj."$($section.Name)"
                    $list = if ($sectionContents -is [IDictionary]) {
                        ([PSCustomObject]$sectionContents).PSObject.Properties
                    }
                    else {
                        $sectionContents.PSObject.Properties
                    }
                    foreach ($item in $list) {
                        Write-Debug "[$($section.Name)] Parsing item: $($item.Name)"
                        $newItem = $item.Value
                        if ($null -eq $newItem.LogicalId) {
                            $newItem | Add-Member -Force -MemberType NoteProperty -Name LogicalId -Value $item.Name -PassThru
                        }
                        switch ($section.Name) {
                            Outputs {
                                if ($item.Name -eq 'Fn::Transform') {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as FnTransform"
                                    $this.AddOutput(($newItem -as [FnTransform]))
                                }
                                else {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as VSOutput"
                                    $this.AddOutput(($newItem -as [VSOutput]))
                                }
                            }
                            Parameters {
                                if ($item.Name -eq 'Fn::Transform') {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as FnTransform"
                                    $this.AddParameter(($newItem -as [FnTransform]))
                                }
                                else {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as VSParameter"
                                    $this.AddParameter(($newItem -as [VSParameter]))
                                }
                            }
                            Mappings {
                                if ($item.Name -eq 'Fn::Transform') {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as FnTransform"
                                    $this.AddMapping(($newItem -as [FnTransform]))
                                }
                                else {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as VSMapping"
                                    $this.AddMapping(([VSMapping]@{LogicalId = $item.Name; Map = $item.Value}))
                                }
                            }
                            Metadata {
                                if ($item.Name -eq 'Fn::Transform') {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as FnTransform"
                                    $this.AddMetadata(($newItem -as [FnTransform]))
                                }
                                else {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as VSMetadata"
                                    $this.AddMetadata(([VSMetadata]@{LogicalId = $item.Name; Metadata = $item.Value}))
                                }
                            }
                            Conditions {
                                if ($item.Name -eq 'Fn::Transform') {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as FnTransform"
                                    $this.AddCondition(($newItem -as [FnTransform]))
                                }
                                else {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as VSCondition"
                                    $this.AddCondition(([VSCondition]@{LogicalId = $item.Name; Condition = $item.Value}))
                                }
                            }
                            Resources {
                                if ($item.Name -eq 'Fn::Transform') {
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Adding as FnTransform"
                                    $this.AddResource(($newItem -as [FnTransform]))
                                }
                                else {
                                    $className = $newItem.Type -replace '^AWS::' -replace '\W'
                                    if ($className -match 'Serverless') {
                                        $className = $className -replace 'Serverless','SAM'
                                    }
                                    Write-Debug "[$($section.Name)] [$($item.Name)] Checking for type: $className"
                                    $resource = if ($t = $className -as [type]) {
                                        Write-Debug "[$($section.Name)] [$($item.Name)] Adding as $className"
                                        $newItem -as $t
                                    }
                                    else {
                                        Write-Debug "[$($section.Name)] [$($item.Name)] Adding as VSResource"
                                        $newItem -as [VSResource]
                                    }
                                    $this.AddResource(($resource))
                                }
                            }
                        }
                        Write-Debug "[$($section.Name)] [$($item.Name)] Added item to VSTemplate"
                    }
                }
                AWSTemplateFormatVersion {
                    $this.AWSTemplateFormatVersion = '2010-09-09'
                }
                default {
                    $this."$($section.Name)" = $baseObj."$($section.Name)"
                }
            }
            Write-Debug "[$($section.Name)] Added section to VSTemplate"
        }
        #>
    }
}

Write-Verbose "Importing class 'ConRef'"

class ConRef : ConditionFunction {
    hidden [string] $_vsFunctionName = 'Add-FnRef'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-or'
    hidden [string] $_topLevelKey = 'Condition'

    [string] $Condition

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name Condition -Value {
            $this[$this._topLevelKey]
        } -SecondValue {
            param([string] $conditionName)
            $this[$this._topLevelKey] = $conditionName
        }
    }

    ConRef() {}

    ConRef([string] $condition) {
        $this[$this._topLevelKey] = $condition
    }

    ConRef([VSCondition] $vsCondition) {
        $this[$this._topLevelKey] = $vsCondition.ToString()
    }
}

Write-Verbose "Importing class 'ConAnd'"

class ConAnd : ConditionFunction {
    hidden [string] $_vsFunctionName = 'Add-ConAnd'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-and'
    hidden [string] $_topLevelKey = 'Fn::And'

    [ValidateCount(2,10)] [object[]] $Conditions

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name Conditions -Value {
            $this[$this._topLevelKey]
        } -SecondValue {
            param([ValidateType(([ConditionFunction]))] [object[]] $value)
            $this[$this._topLevelKey] = $value
        }
    }

    hidden [void] _validateInput([object[]] $inputData) {
        if ($inputData.Count -lt 2 -or $inputData.Count -gt 10) {
            throw [VSError]::InvalidArgument($inputData, "$($inputData.Count) condition(s) provided! The minimum number of conditions that you can include is 2, and the maximum is 10.")
        }
    }

    ConAnd() : base() {}
    ConAnd([object[]] $conditions) : base($conditions) {}
}

Write-Verbose "Importing class 'ConEquals'"
class ConEquals : ConditionFunction {
    hidden [string] $_vsFunctionName = 'Add-ConEquals'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-equals'
    hidden [string] $_topLevelKey = 'Fn::Equals'

    [object] $ValueOne
    [object] $ValueTwo

    hidden [void] _validateInput([object[]] $inputData) {
        if ($inputData.Count -ne 2) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 2. Count provided: $($inputData.Count)")
        }
    }

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name ValueOne -Value {
            $this[$this._topLevelKey] | Select-Object -First 1
        } -SecondValue {
            param([object] $value)
            $clean = if ($value | Get-Member -Name ToOrderedDictionary* -MemberType Method -ErrorAction SilentlyContinue) {
                $value.ToOrderedDictionary()
            }
            else {
                $value
            }
            if ($null -eq $this[$this._topLevelKey]) {
                $this[$this._topLevelKey] = @($clean)
            }
            else {
                if ($this[$this._topLevelKey].Count -ge 1) {
                    $this[$this._topLevelKey][0] = $clean
                }
                else {
                    $this[$this._topLevelKey] += $clean
                }
            }
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name ValueTwo -Value {
            $this[$this._topLevelKey] | Select-Object -First 1
        } -SecondValue {
            param([object] $value)
            $clean = if ($value | Get-Member -Name ToOrderedDictionary* -MemberType Method -ErrorAction SilentlyContinue) {
                $value.ToOrderedDictionary()
            }
            else {
                $value
            }
            if ($null -eq $this[$this._topLevelKey]) {
                $this[$this._topLevelKey] = @($null,$clean)
            }
            else {
                if ($this[$this._topLevelKey].Count -gt 1) {
                    $this[$this._topLevelKey][1] = $clean
                }
                else {
                    $this[$this._topLevelKey] += $clean
                }
            }
        }
    }

    ConEquals() : base() {}
    ConEquals([object[]] $conditions) : base($conditions) {}
    ConEquals(
        [object] $value1,
        [object] $value2
    ) {
        $this[$this._topLevelKey] = @($value1,$value2)
    }
}

Write-Verbose "Importing class 'ConIf'"
class ConIf : ConditionFunction {
    hidden [string] $_vsFunctionName = 'Add-ConIf'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-if'
    hidden [string] $_topLevelKey = 'Fn::If'
    hidden [type[]] $_validTypes = @( [string], [int], [bool], [IDictionary], [psobject], [VSObject], [VSHashtable] )

    hidden [void] _validateInput([object[]] $inputData) {
        if ($inputData.Count -ne 3) {
            throw [VSError]::InvalidArgument($inputData, "Total input item count when constructing a <$($this.GetType())> object needs to be 3. Count provided: $($inputData.Count)")
        }
        elseif ($inputData[0] -isnot [string]) {
            throw [VSError]::InvalidArgument($inputData, "The first item provided when constructing a <$($this.GetType())> object needs to be a string. Type provided: $($inputData[0].GetType())")
        }
    }

    ConIf() : base() {}
    ConIf([object[]] $conditions) : base($conditions) {}

    ConIf(
        [string] $conditionName,
        [object] $valueIfTrue,
        [object] $valueIfFalse
    ) {
        $final = @($conditionName)
        foreach ($item in @($valueIfTrue,$valueIfFalse)) {
            if ($item | Get-Member -Name ToOrderedDictionary* -MemberType Method -ErrorAction SilentlyContinue) {
                $final += $item.ToOrderedDictionary()
            }
            else {
                $final += $item
            }
        }
        $this[$this._topLevelKey] = $final
    }
}

Write-Verbose "Importing class 'ConNot'"
class ConNot : ConditionFunction {
    hidden [string] $_vsFunctionName = 'Add-ConNot'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-not'
    hidden [string] $_topLevelKey = 'Fn::Not'

    [object[]] $Conditions

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name Conditions -Value {
            $this[$this._topLevelKey]
        } -SecondValue {
            param([ValidateType(([ConditionFunction]))] [object[]] $value)
            $this[$this._topLevelKey] = $value
        }
    }

    ConNot() {}
    ConNot([object[]] $conditions) : base($conditions) {}
}

Write-Verbose "Importing class 'ConOr'"
class ConOr : ConditionFunction {
    hidden [string] $_vsFunctionName = 'Add-ConOr'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-or'
    hidden [string] $_topLevelKey = 'Fn::Or'

    [object[]] $Conditions

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name Conditions -Value {
            $this[$this._topLevelKey]
        } -SecondValue {
            param([ValidateType(([ConditionFunction]))] [object[]] $value)
            $this[$this._topLevelKey] = $value
        }
    }

    ConOr() {}
    ConOr([object[]] $conditions) : base($conditions) {}
}

Write-Verbose "Importing class 'UserData'"

class UserData : FnBase64 {
    hidden [string] $_vsFunctionName = 'Add-UserData'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/deploying.applications.html'

    static [object] Transform([UserData] $userData) {
        Write-Debug "Transforming UserData from [UserData]"
        return $userData['Fn::Base64']
    }

    static [object] Transform([FnJoin] $fnJoin) {
        Write-Debug "Transforming UserData from [FnJoin]"
        return $fnJoin
    }

    static [object] Transform([FnBase64] $fnBase64) {
        Write-Debug "Transforming UserData from [FnBase64]"
        return $fnBase64['Fn::Base64']
    }

    static [object] Transform([string] $userDataStringOrFilepath) {
        Write-Debug "Transforming UserData from [string]"
        return [UserData]::Transform($false, $false, @{}, $userDataStringOrFilepath)
    }

    static [object] Transform([string[]] $userDataStringOrFilepath) {
        Write-Debug "Transforming UserData from [string[]]"
        return [UserData]::Transform($false, $false, @{}, ($userDataStringOrFilepath -join [Environment]::NewLine))
    }

    static [object] Transform([string] $userDataStringOrFilepath, [bool] $persist) {
        Write-Debug "Transforming UserData from [string] with Persist=$persist"
        return [UserData]::Transform($persist, $false, @{}, $userDataStringOrFilepath)
    }

    static [object] Transform([bool] $useJoin, [string] $userDataStringOrFilepath) {
        Write-Debug "Transforming UserData from [string] with UseJoin=$useJoin"
        return [UserData]::Transform($false, $useJoin, @{}, $userDataStringOrFilepath)
    }

    static [object] Transform([bool] $persist, [bool] $useJoin, [IDictionary] $replaceDict, [string] $userDataStringOrFilepath) {
        $final = @()
        $startTag = $null
        $endTag = $null
        $tag = $null
        if (Test-Path $userDataStringOrFilepath) {
            Write-Debug "Extracting script from file path: $userDataStringOrFilepath"
            $item = Get-Item $userDataStringOrFilepath
            $tag = switch -RegEx ($item.Extension) {
                '^\.ps1$' {
                    "powershell"
                }
                '^\.(bat|cmd)$' {
                    "script"
                }
                default {
                    $null
                }
            }
            $fileContents = [File]::ReadAllLines($item.FullName)
            if ($tag -and ($fileContents -join [Environment]::NewLine) -notlike "<$($tag)>*") {
                if ($fileContents[0] -notlike "<$($tag)>`n*") {
                    Write-Debug "Adding missing script tags: <$tag>"
                    $final += "<$($tag)>"
                }
            }
            $final += $fileContents
            if ($tag -and ($fileContents -join [Environment]::NewLine) -notlike "*</$($tag)>*") {
                $final += "</$($tag)>"
            }
            if ($persist -and ($fileContents -join [Environment]::NewLine) -notlike "*<persist>true</persist>*") {
                Write-Debug "Adding missing script tags: <persist>"
                $final += "`n<persist>true</persist>"
            }
        }
        else {
            $final += $userDataStringOrFilepath
        }
        $replaceDict.GetEnumerator() | ForEach-Object {
            Write-Verbose "Replacing '$($_.Key)' with '$($_.Value)'"
            $final = $final.Replace($_.Key,$_.Value)
        }
        if ($null -ne $tag -and ($final -join [Environment]::NewLine) -notmatch "\<$tag\>") {
            $final.Insert(0,"<$($tag)>") | Out-Null
            $final += "</$($tag)>"
        }
        if ($useJoin) {
            return [FnJoin]::new([Environment]::NewLine,$final)
        }
        else {
            return ($final -join [Environment]::NewLine)
        }
    }

    UserData() : base() {}
    UserData([IDictionary] $props) : base($props) {}
    UserData([psobject] $props) : base($props) {}

    UserData([FnJoin] $fnJoin) {
        Write-Debug "Creating UserData from [FnJoin]"
        $this['Fn::Base64'] = $fnJoin
    }
    UserData([FnBase64] $fnBase64) {
        Write-Debug "Creating UserData from [FnBase64]"
        $this['Fn::Base64'] = $fnBase64['Fn::Base64']
    }
    UserData([string] $userDataStringOrFilepath) {
        Write-Debug "Creating UserData from [string]"
        $this['Fn::Base64'] = [UserData]::Transform($userDataStringOrFilepath)
    }
    UserData([bool] $useJoin, [string] $userDataStringOrFilepath) {
        Write-Debug "Creating UserData from [string] with UseJoin=$useJoin"
        $this['Fn::Base64'] = [UserData]::Transform($useJoin,$userDataStringOrFilepath)
    }
<# UserData([object] $object) {
        Write-Debug "Creating UserData from [object]"
        $this['Fn::Base64'] = [FnJoin]::new([Environment]::NewLine,$object)
    } #>

    UserData([object[]] $objects) {
        Write-Debug "Creating UserData from [object[]]"
        $final = @()
        $tag = $null
        foreach ($o in $objects) {
            if ($o -is [string] -and (Test-Path $o)) {
                Write-Debug "Extracting script from file path: $o"
                $item = Get-Item $o
                $tag = switch -RegEx ($item.Extension) {
                    '^\.ps1$' {
                        "powershell"
                    }
                    '^\.(bat|cmd)$' {
                        "script"
                    }
                    default {
                        $null
                    }
                }
                [File]::ReadAllLines($item.FullName) | ForEach-Object {
                    $final += $_
                }
            }
            else {
                $final += $o
            }
        }
        if ($null -ne $tag -and ($final -join [Environment]::NewLine) -notmatch "\<$tag\>") {
            $final.Insert(0,"<$($tag)>") | Out-Null
            $final += "</$($tag)>"
        }
        $this['Fn::Base64'] = [FnJoin]::new([Environment]::NewLine,$final)
    }
    UserData([bool] $useJoin, [object[]] $objects) {
        Write-Debug "Creating UserData from [object[]] with UseJoin=$useJoin"
        if ($useJoin -or $null -ne ($objects | Where-Object {$_ -isnot [string]})) {
            $this['Fn::Base64'] = [FnJoin]::new([Environment]::NewLine,$objects)
        }
        else {
            Write-Debug "All objects are strings and UseJoin=$useJoin"
            $this['Fn::Base64'] = $objects -join [Environment]::NewLine
        }
    }
    UserData([UserData] $userData) {
        Write-Debug "Creating UserData from [UserData]"
        $this['Fn::Base64']  = $userData['Fn::Base64']
    }
}

Write-Verbose "Importing class 'AutoScalingCreationPolicy'"

class AutoScalingCreationPolicy : VSObject {
    hidden [string] $_vsFunctionName = 'Add-CreationPolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-creationpolicy.html#cfn-attributes-creationpolicy-autoscalingcreationpolicy'

    hidden [object] $_minSuccessfulInstancesPercent

    [ValidateRange(0,100)] [int]
    $MinSuccessfulInstancesPercent

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name MinSuccessfulInstancesPercent -Value {
            $this._minSuccessfulInstancesPercent
        } -SecondValue {
            param(
                [object]
                $minSuccessfulInstancesPercent
            )
            if (
                $minSuccessfulInstancesPercent -as [int] -and
                (
                    ($minSuccessfulInstancesPercent -as [int]) -gt 100 -or
                    ($minSuccessfulInstancesPercent -as [int]) -lt 0
                )
            ) {
                $errorRecord = [VSError]::new(
                    [ArgumentException]::new("MinSuccessfulInstancesPercent must be an integer between 0-100!"),
                    'InvalidMinSuccessfulInstancesPercent',
                    [ErrorCategory]::InvalidArgument,
                    $minSuccessfulInstancesPercent
                )
                throw [VSError]::InsertError($errorRecord)
            }
            if ($cast = $minSuccessfulInstancesPercent -as [int]) {
                $this._minSuccessfulInstancesPercent = $cast
            }
            elseif ($value -is [IntrinsicFunction] -or $value -is [ConditionFunction]) {
                $this._minSuccessfulInstancesPercent = $minSuccessfulInstancesPercent
            }
            else {
                throw [VSError]::InvalidArgument($minSuccessfulInstancesPercent,"$($this.GetType()) - Invalid value for property MinSuccessfulInstancesPercent")
            }
        }
    }

    AutoScalingCreationPolicy() : base() {}
    AutoScalingCreationPolicy([IDictionary] $props) : base($props) {}
    AutoScalingCreationPolicy([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'AutoScalingReplacingUpdate'"

class AutoScalingReplacingUpdate : VSObject {
    hidden [string] $_vsFunctionName = 'Add-UpdatePolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-replacingupdate'

    [nullable[bool]] $WillReplace = $null

    AutoScalingReplacingUpdate() : base() {}
    AutoScalingReplacingUpdate([bool] $willReplace) {
        $this.WillReplace = $willReplace
    }
    AutoScalingReplacingUpdate([IDictionary] $props) : base($props) {}
    AutoScalingReplacingUpdate([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'AutoScalingRollingUpdate'"

class AutoScalingRollingUpdate : VSObject {
    hidden [string] $_vsFunctionName = 'Add-UpdatePolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-replacingupdate'

    hidden [object] $_maxBatchSize
    hidden [object] $_minInstancesInService
    hidden [object] $_minSuccessfulInstancesPercent
    hidden [object] $_pauseTime
    hidden [string[]] $_suspendProcesses = $null

    [int] $MaxBatchSize
    [int] $MinInstancesInService
    [int] $MinSuccessfulInstancesPercent
    [string] $PauseTime
    [AutoScalingProcess[]] $SuspendProcesses
    [nullable[bool]] $WaitOnResourceSignals = $null

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name MaxBatchSize -Value {
            $this._maxBatchSize
        } -SecondValue {
            param([object] $value)
            if ($null -ne ($value -as [int])) {
                $this._maxBatchSize = $value -as [int]
            }
            elseif ($value -is [IntrinsicFunction] -or $value -is [ConditionFunction]) {
                $this._maxBatchSize = $value
            }
            else {
                throw [VSError]::InvalidArgument($value,"$($this.GetType()) - Invalid value for property MaxBatchSize")
            }
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name MinInstancesInService -Value {
            $this._minInstancesInService
        } -SecondValue {
            param([object] $value)
            if ($null -ne ($value -as [int])) {
                $this._minInstancesInService = $value -as [int]
            }
            elseif ($value -is [IntrinsicFunction] -or $value -is [ConditionFunction]) {
                $this._minInstancesInService = $value
            }
            else {
                throw [VSError]::InvalidArgument($value,"$($this.GetType()) - Invalid value for property MinInstancesInService")
            }
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name MinSuccessfulInstancesPercent -Value {
            $this._minSuccessfulInstancesPercent
        } -SecondValue {
            param([object] $value)
            if ($null -ne ($value -as [int])) {
                $this._minSuccessfulInstancesPercent = $value -as [int]
            }
            elseif ($value -is [IntrinsicFunction] -or $value -is [ConditionFunction]) {
                $this._minSuccessfulInstancesPercent = $value
            }
            else {
                throw [VSError]::InvalidArgument($value,"$($this.GetType()) - Invalid value for property MinSuccessfulInstancesPercent")
            }
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name PauseTime -Value {
            $this._pauseTime
        } -SecondValue {
            param(
                [ValidateType(([string], [TimeSpan], [IntrinsicFunction], [ConditionFunction]))] [object]
                $value
            )
            if ($ts = $value -as [TimeSpan]) {
                $pt = 'P'
                if ($ts.Days) {
                    $pt += ("{0}D" -f $ts.Days)
                }
                if ($ts.Hours + $ts.Minutes + $ts.Seconds) {
                    $pt += 'T'
                    if ($ts.Hours) {
                        $pt += ("{0}H" -f $ts.Hours)
                    }
                    if ($ts.Minutes) {
                        $pt += ("{0}M" -f $ts.Minutes)
                    }
                    if ($ts.Seconds) {
                        $pt += ("{0}S" -f $ts.Seconds)
                    }
                }
                $this._pauseTime = $pt
            }
            elseif ($value -is [string] -and $value -notmatch '^P((?<Years>[\d\.,]+)Y)?((?<Months>[\d\.,]+)M)?((?<Weeks>[\d\.,]+)W)?((?<Days>[\d\.,]+)D)?(?<Time>T((?<Hours>[\d\.,]+)H)?((?<Minutes>[\d\.,]+)M)?((?<Seconds>[\d\.,]+)S)?)?$') {
                $errorRecord = [VSError]::new(
                    [ArgumentException]::new("Value '$value' is not a valid ISO8601 duration string!"),
                    'InvalidPauseTime',
                    [ErrorCategory]::InvalidArgument,
                    $value
                )
                throw [VSError]::InsertError($errorRecord)
            }
            else {
                $this._pauseTime = $value
            }
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name SuspendProcesses -Value {
            $this._suspendProcesses
        } -SecondValue {
            param(
                [AutoScalingProcess[]] $value
            )
            if ($null -eq $this._suspendProcesses) {
                $this._suspendProcesses = @()
            }
            foreach ($proc in $value) {
                $this._suspendProcesses += $proc.ToString()
            }
        }
    }

    AutoScalingRollingUpdate() : base() {}
    AutoScalingRollingUpdate([IDictionary] $props) : base($props) {}
    AutoScalingRollingUpdate([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'AutoScalingScheduledAction'"

class AutoScalingScheduledAction : VSObject {
    hidden [string] $_vsFunctionName = 'Add-UpdatePolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-scheduledactions'

    [nullable[bool]] $IgnoreUnmodifiedGroupSizeProperties = $null

    AutoScalingScheduledAction() : base() {}
    AutoScalingScheduledAction([bool] $ignoreUnmodifiedGroupSizeProperties) {
        $this.IgnoreUnmodifiedGroupSizeProperties = $ignoreUnmodifiedGroupSizeProperties
    }
    AutoScalingScheduledAction([IDictionary] $props) : base($props) {}
    AutoScalingScheduledAction([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'CodeDeployLambdaAliasUpdate'"

class CodeDeployLambdaAliasUpdate : VSObject {
    hidden [string] $_vsFunctionName = 'Add-UpdatePolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-codedeploylambdaaliasupdate'

    hidden [object] $_afterAllowTrafficHook
    hidden [object] $_applicationName
    hidden [object] $_beforeAllowTrafficHook
    hidden [object] $_deploymentGroupName

    [string] $AfterAllowTrafficHook
    [string] $ApplicationName
    [string] $BeforeAllowTrafficHook
    [string] $DeploymentGroupName

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name AfterAllowTrafficHook -Value {
            $this._afterAllowTrafficHook
        } -SecondValue {
            param(
                [ValidateType(([string], [IntrinsicFunction], [ConditionFunction]))] [object]
                $value
            )
            $this._afterAllowTrafficHook = $value
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name ApplicationName -Value {
            $this._applicationName
        } -SecondValue {
            param(
                [ValidateType(([string], [IntrinsicFunction], [ConditionFunction]))] [object]
                $value
            )
            $this._applicationName = $value
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name BeforeAllowTrafficHook -Value {
            $this._beforeAllowTrafficHook
        } -SecondValue {
            param(
                [ValidateType(([string], [IntrinsicFunction], [ConditionFunction]))] [object]
                $value
            )
            $this._beforeAllowTrafficHook = $value
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name DeploymentGroupName -Value {
            $this._deploymentGroupName
        } -SecondValue {
            param(
                [ValidateType(([string], [IntrinsicFunction], [ConditionFunction]))] [object]
                $value
            )
            $this._deploymentGroupName = $value
        }
    }

    CodeDeployLambdaAliasUpdate() : base() {}
    CodeDeployLambdaAliasUpdate([IDictionary] $props) : base($props) {}
    CodeDeployLambdaAliasUpdate([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'ResourceSignal'"

class ResourceSignal : VSObject {
    hidden [string] $_vsFunctionName = 'Add-CreationPolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-creationpolicy.html#cfn-attributes-creationpolicy-resourcesignal'

    hidden [object] $_count
    hidden [object] $_timeout

    [int] $Count
    [string] $Timeout

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name Count -Value {
            $this._count
        } -SecondValue {
            param([object] $count)
            if ($cast = $count -as [int]) {
                $this._count = $cast
            }
            elseif ($count -is [IntrinsicFunction] -or $count -is [ConditionFunction]) {
                $this._count = $count
            }
            else {
                throw [VSError]::InvalidArgument($count,"Count must be an integer or a Condition or Intrinsic function.")
            }
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name Timeout -Value {
            $this._timeout
        } -SecondValue {
            param([object] $timeout)
            if ($timeout -is [string]) {
                try {
                    # Check if it's a valid ISO8601 duration string
                    $null = [XmlConvert]::ToTimeSpan($timeOut)
                    $this._timeout = $timeout
                }
                catch {
                    throw [VSError]::InvalidArgument($timeout,"Timeout must be a valid ISO8601 duration string or an Intrinsic or Condition Function object!")
                }
            }
            elseif ($count -is [IntrinsicFunction] -or $count -is [ConditionFunction]) {
                $this._timeout = $timeout
            }
            else {
                throw [VSError]::InvalidArgument($timeout,"Timeout must be a valid ISO8601 duration string or an Intrinsic or Condition Function object!")
            }
        }
    }

    ResourceSignal() : base() {}
    ResourceSignal([IDictionary] $props) : base($props) {}
    ResourceSignal([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'CreationPolicy'"

class CreationPolicy : VSObject {
    hidden [string] $_vsFunctionName = 'Add-CreationPolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-creationpolicy.html'

    [AutoScalingCreationPolicy] $AutoScalingCreationPolicy
    [ResourceSignal] $ResourceSignal

    CreationPolicy() : base() {}
    CreationPolicy([IDictionary] $props) : base($props) {}
    CreationPolicy([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'UpdatePolicy'"

class UpdatePolicy : VSObject {
    hidden [string] $_vsFunctionName = 'Add-UpdatePolicy'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html'

    [AutoScalingReplacingUpdate] $AutoScalingReplacingUpdate
    [AutoScalingRollingUpdate] $AutoScalingRollingUpdate
    [AutoScalingScheduledAction] $AutoScalingScheduledAction
    [CodeDeployLambdaAliasUpdate] $CodeDeployLambdaAliasUpdate
    [nullable[bool]] $EnableVersionUpgrade = $null
    [nullable[bool]] $UseOnlineResharding = $null

    UpdatePolicy() : base() {}
    UpdatePolicy([IDictionary] $props) : base($props) {}
    UpdatePolicy([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'Export'"

class Export : VSObject {
    hidden [string] $_vsFunctionName = 'New-VaporOutput'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html'

    hidden [object] $_name

    [string] $Name

    [string] ToString() {
        return $this._name.ToString()
    }

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Name' -Value {
            $this._name
        } -SecondValue {
            param([ValidateType(([string], [IntrinsicFunction]))] [object] $value)
            $this._name = $value
        }
    }

    Export([object] $name) {
        $this._addAccessors()
        $this.Name = $name
    }

    Export([string] $name) {
        $this._addAccessors()
        $this.Name = $name
    }

    Export() : base() {}
    Export([IDictionary] $props) : base($props) {}
    Export([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'VSCondition'"

class VSCondition : VSHashtable {
    hidden [string] $_vsFunctionName = 'New-VaporCondition'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html'

    [ValidateLogicalId()] [string] $LogicalId
    [ConditionFunction] $Condition

    [string] ToString() {
        return $this.LogicalId
    }

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name 'Condition' -Value {
            $this.ToOrderedDictionary()
        } -SecondValue {
            param([object] $value)
            if ($value -isnot [IDictionary] -and $value -isnot [psobject]) {
                throw [VSError]::InvalidType($value,@([IDictionary],[psobject]))
            }
            if ($value -is [IDictionary]) {
                $value.GetEnumerator() | ForEach-Object {
                    $this[$_.Key] = $_.Value
                }
            }
            elseif ($value -is [psobject]) {
                $value.PSObject.Properties | ForEach-Object {
                    $this[$_.Name] = $_.Value
                }
            }
        }
    }

    VSCondition() : base() {}
    VSCondition([IDictionary] $props) {
        $mainKey = $props.Keys | Select-Object -First 1
        $mainValue = $props.Values | Select-Object -First 1
        if (
            $null -ne $props.LogicalId -and
            $null -ne $props.Condition -and
            (
                $props.Condition -is [ConditionFunction] -or
                $null -ne $props.Condition.'Fn::And' -or
                $null -ne $props.Condition.'Fn::Equals' -or
                $null -ne $props.Condition.'Fn::If' -or
                $null -ne $props.Condition.'Fn::Not' -or
                $null -ne $props.Condition.'Fn::Or'
            )
        ) {
            Write-Debug "Creating VSCondition from correctly formed IDictionary"
            $this.LogicalId = $props.LogicalId
            $this.Condition = $props.Condition
        }
        elseif (
            $props.Keys.Count -eq 1 -and
            (
                $mainValue -is [ConditionFunction] -or
                $null -ne $mainValue.'Fn::And' -or
                $null -ne $mainValue.'Fn::Equals' -or
                $null -ne $mainValue.'Fn::If' -or
                $null -ne $mainValue.'Fn::Not' -or
                $null -ne $mainValue.'Fn::Or'
            )
        ) {
            Write-Debug "Creating VSCondition from raw IDictionary"
            $this.LogicalId = $mainKey
            $this.Condition = $mainValue
        }
        else {
            throw [VSError]::InvalidArgument($props,"Input IDictionary did not match expected contents, unable to construct a VSCondition object.")
        }
    }
    VSCondition([psobject] $props) {
        $mainKey = $props.PSObject.Properties.Name | Select-Object -First 1
        $mainValue = $props.PSObject.Properties.Value | Select-Object -First 1
        if (
            $null -ne $props.LogicalId -and
            $null -ne $props.Condition -and
            (
                $props.Condition -is [ConditionFunction] -or
                $null -ne $props.Condition.'Fn::And' -or
                $null -ne $props.Condition.'Fn::Equals' -or
                $null -ne $props.Condition.'Fn::If' -or
                $null -ne $props.Condition.'Fn::Not' -or
                $null -ne $props.Condition.'Fn::Or'
            )
        ) {
            Write-Debug "Creating VSCondition from correctly formed PSObject"
            $this.LogicalId = $props.LogicalId
            $this.Condition = $props.Condition
        }
        elseif (
            $props.PSObject.Properties.Name.Count -eq 1 -and
            (
                $mainValue -is [ConditionFunction] -or
                $null -ne $mainValue.'Fn::And' -or
                $null -ne $mainValue.'Fn::Equals' -or
                $null -ne $mainValue.'Fn::If' -or
                $null -ne $mainValue.'Fn::Not' -or
                $null -ne $mainValue.'Fn::Or'
            )
        ) {
            Write-Debug "Creating VSCondition from raw PSObject"
            $this.LogicalId = $mainKey
            $this.Condition = $mainValue
        }
        else {
            throw [VSError]::InvalidArgument($props,"Input PSObject did not match expected contents, unable to construct a VSCondition object.")
        }
    }
}

Write-Verbose "Importing class 'VSMapping'"

class VSMapping : VSLogicalObject {
    hidden [string] $_vsFunctionName = 'New-VaporMapping'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html'

    hidden [object] $_map

    [IDictionary] $Map

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name 'Map' -Value {
            $this._map
        } -SecondValue {
            param([object] $map)
            $ht = [ordered]@{}
            if ($map -is [psobject]) {
                $map.PSObject.Properties | ForEach-Object {
                    if ($_.Name -notmatch 'LogicalId' -and $_.Value -isnot [IDictionary] -and $_.Value -isnot [psobject]) {
                        throw [VSError]::InvalidMap($map, $_.Name, $_.Value)
                    }
                    if ($_.Name -eq 'LogicalId') {
                        $this.LogicalId = $_.Value
                    }
                    else {
                        $ht[$_.Name] = $_.Value
                    }
                }
            }
            elseif ($map -is [IDictionary]) {
                $map.GetEnumerator() | ForEach-Object {
                    if ($_.Key -notmatch 'LogicalId' -and $_.Value -isnot [IDictionary] -and $_.Value -isnot [psobject]) {
                        throw [VSError]::InvalidMap($map, $_.Key, $_.Value)
                    }
                    if ($_.Key -eq 'LogicalId') {
                        $this.LogicalId = $_.Value
                    }
                    else {
                        $ht[$_.Key] = $_.Value
                    }
                }
            }
            else {
                throw [VSError]::InvalidArgument($map,"The Map value must be an IDictionary or PSObject!")
            }
            $this._map = $ht
        }
    }

    VSMapping() : base() {}
    VSMapping([IDictionary] $props) : base($props) {}
    VSMapping([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'VSMetadata'"

class VSMetadata : VSHashtable {
    hidden [string] $_vsFunctionName = 'New-VaporMetadata'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html'

    [ValidateLogicalId()] [string] $LogicalId
    [object] $Metadata

    [string] ToString() {
        return $this.LogicalId
    }

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name 'Metadata' -Value {
            $this.ToOrderedDictionary()
        } -SecondValue {
            param([object] $value)
            if ($value -isnot [IDictionary] -and $value -isnot [psobject]) {
                throw [VSError]::InvalidType($value,@([IDictionary],[psobject]))
            }
            if ($value -is [IDictionary]) {
                $value.GetEnumerator() | ForEach-Object {
                    $this[$_.Key] = $_.Value
                }
            }
            elseif ($value -is [psobject]) {
                $value.PSObject.Properties | ForEach-Object {
                    $this[$_.Name] = $_.Value
                }
            }
        }
    }

    VSMetadata() : base() {}
    VSMetadata([IDictionary] $props) {
        $mainKey = $props.Keys | Select-Object -First 1
        $mainValue = $props.Values | Select-Object -First 1
        if (
            $null -ne $props.LogicalId -and
            $null -ne $props.Metadata
        ) {
            Write-Debug "Creating VSMetadata from correctly formed IDictionary"
            $this.LogicalId = $props.LogicalId
            $this.Metadata = $props.Metadata
        }
        elseif (
            $props.Keys.Count -eq 1
        ) {
            Write-Debug "Creating VSMetadata from raw IDictionary"
            $this.LogicalId = $mainKey
            $this.Metadata = $mainValue
        }
        else {
            throw [VSError]::InvalidArgument($props,"Input IDictionary did not match expected contents, unable to construct a VSMetadata object.")
        }
    }
    VSMetadata([psobject] $props) {
        $mainKey = $props.PSObject.Properties.Name | Select-Object -First 1
        $mainValue = $props.PSObject.Properties.Value | Select-Object -First 1
        if (
            $null -ne $props.LogicalId -and
            $null -ne $props.Metadata
        ) {
            Write-Debug "Creating VSMetadata from correctly formed PSObject"
            $this.LogicalId = $props.LogicalId
            $this.Metadata = $props.Metadata
        }
        elseif (
            $props.PSObject.Properties.Name.Count -eq 1
        ) {
            Write-Debug "Creating VSMetadata from raw PSObject"
            $this.LogicalId = $mainKey
            $this.Metadata = $mainValue
        }
        else {
            throw [VSError]::InvalidArgument($props,"Input PSObject did not match expected contents, unable to construct a VSMetadata object.")
        }
    }
}

Write-Verbose "Importing class 'VSOutput'"

class VSOutput : VSLogicalObject {
    hidden [string] $_vsFunctionName = 'New-VaporOutput'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html'

    hidden [object] $_value
    hidden [object] $_condition

    [string] $Condition
    [string] $Description
    [string] $Value
    [Export] $Export

    hidden [void] _addAccessors() {
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Value' -Value {
            $this._value
        } -SecondValue {
            param([ValidateType(([string], [int], [bool], [hashtable], [psobject], [IntrinsicFunction], [ConditionFunction]))] [object] $value)
            $this._value = $value
        }
        $this | Add-Member -Force -MemberType 'ScriptProperty' -Name 'Condition' -Value {
            $this._condition
        } -SecondValue {
            param([ValidateType(([string], [hashtable], [psobject], [ConRef]))] [object] $value)
            $this._condition = if ($value -is [ConRef]) {
                $value.Condition
            }
            else {
                $value
            }
        }
    }

    VSOutput() : base() {}
    VSOutput([IDictionary] $props) : base($props) {}
    VSOutput([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'VSParameter'"

class VSParameter : VSLogicalObject {
    hidden [string] $_vsFunctionName = 'New-VaporParameter'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html'

    [string] $AllowedPattern
    [string[]] $AllowedValues
    [string] $ConstraintDescription
    [string] $Default
    [ValidateLength(0,4000)] [string] $Description
    [nullable[int]] $MaxLength = $null
    [nullable[int]] $MaxValue = $null
    [nullable[int]] $MinLength = $null
    [nullable[int]] $MinValue = $null
    [nullable[bool]] $NoEcho = $null
    [ValidateSet("String","Number","List<Number>","CommaDelimitedList","AWS::EC2::AvailabilityZone::Name","AWS::EC2::Image::Id","AWS::EC2::Instance::Id","AWS::EC2::KeyPair::KeyName","AWS::EC2::SecurityGroup::GroupName","AWS::EC2::SecurityGroup::Id","AWS::EC2::Subnet::Id","AWS::EC2::Volume::Id","AWS::EC2::VPC::Id","AWS::Route53::HostedZone::Id","List<AWS::EC2::AvailabilityZone::Name>","List<AWS::EC2::Image::Id>","List<AWS::EC2::Instance::Id>","List<AWS::EC2::SecurityGroup::GroupName>","List<AWS::EC2::SecurityGroup::Id>","List<AWS::EC2::Subnet::Id>","List<AWS::EC2::Volume::Id>","List<AWS::EC2::VPC::Id>","List<AWS::Route53::HostedZone::Id>")] [string] $Type

    VSParameter() : base() {}
    VSParameter([IDictionary] $props) : base($props) {}
    VSParameter([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'VSResourceProperty'"

class VSResourceProperty : VSObject {
    hidden [string] $_vsFunctionName = 'New-VaporResource'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html'

    VSResourceProperty() : base() {}
    VSResourceProperty([IDictionary] $props) : base($props) {}
    VSResourceProperty([psobject] $props) : base($props) {}
}

Write-Verbose "Importing class 'VSResource'"

class VSResource : VSLogicalObject {
    hidden [string] $_vsFunctionName = 'New-VaporResource'
    hidden [string] $_awsDocumentation = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html'

    hidden [string[]] $_attributes = @()
    hidden [object] $_deletionPolicy
    hidden [object] $_updateReplacePolicy

    [string] $Condition
    [CreationPolicy] $CreationPolicy
    [string[]] $DependsOn
    [TransformDeletionPolicy()] [DeletionPolicy] $DeletionPolicy
    [VSJson] $Metadata
    [VSHashtable] $Properties = [VSHashtable]::new()
    [string] $Type
    [UpdatePolicy] $UpdatePolicy
    [TransformDeletionPolicy()] [UpdateReplacePolicy] $UpdateReplacePolicy

    static [object] FormatDeletionPolicy([object] $policy) {
        if ($policy -is [string]) {
                return (Get-Culture).TextInfo.ToTitleCase($policy.ToString().ToLower())
        }
        else {
            return $policy
        }
    }

    [FnRef] Ref() {
        return [FnRef]::new($this.LogicalId)
    }

    [FnGetAtt] GetAtt() {
        if ($this._attributes.Count -ne 1) {
            $message = if ($this._attributes.Count -eq 0) {
                "There are 0 attributes available for this resource! Please use <FnRef> instead."
            }
            else {
                "There are $($this._attributes.Count) attributes available for this resource! Please specify the attribute you would like to use for <FnGetAtt> instead. Valid attributes for a $($this.GetType().FullName) resource: $($this._attributes -join ', ')"
            }
            $errorRecord = [VSError]::new(
                [ArgumentException]::new($message),
                'InvalidAttribute',
                [ErrorCategory]::InvalidArgument,
                $this._attributes
            )
            throw [VSError]::InsertError($errorRecord)
        }
        return [FnGetAtt]::new($this.LogicalId, ($this._attributes | Select-Object -First 1))
    }

    [FnGetAtt] GetAtt([string] $attributeName) {
        if ($attributeName -notin $this._attributes) {
            $message = if ($this._attributes.Count -eq 0) {
                "There are 0 attributes available for this resource! Please use <FnRef> instead."
            }
            else {
                "$attributeName is not a valid attribute for this resource to return via <FnGetAtt>. Valid attributes for a $($this.GetType().FullName) resource: $($this._attributes -join ', ')"
            }
            $errorRecord = [VSError]::new(
                [ArgumentException]::new($message),
                'InvalidAttribute',
                [ErrorCategory]::InvalidArgument,
                $attributeName
            )
            throw [VSError]::InsertError($errorRecord)
        }
        return [FnGetAtt]::new($this.LogicalId, $attributeName)
    }

    hidden [void] _addBaseAccessors() {
        $this | Add-Member -Force -MemberType ScriptProperty -Name DeletionPolicy -Value {
            $this::FormatDeletionPolicy($this._deletionPolicy)
        } -SecondValue {
            param(
                [ValidateType(([string], [IntrinsicFunction], [DeletionPolicy]))] [object]
                $deletionPolicy
            )
            $this._deletionPolicy = $deletionPolicy
        }
        $this | Add-Member -Force -MemberType ScriptProperty -Name UpdateReplacePolicy -Value {
            $this::FormatDeletionPolicy($this._updateReplacePolicy)
        } -SecondValue {
            param(
                [ValidateType(([string], [IntrinsicFunction], [UpdateReplacePolicy]))] [object]
                $updateReplacePolicy
            )
            $this._updateReplacePolicy = $updateReplacePolicy
        }
    }

    VSResource() : base() {}
    VSResource([IDictionary] $props) : base($props) {}
    VSResource([psobject] $props) : base($props) {}
}