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.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 '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 [VSJson] Transform([string] $jsonStringOrFilePath) {
        $newData = if (Test-Path $jsonStringOrFilePath) {
            $resolvedPath = (Resolve-Path $jsonStringOrFilePath).Path
            [File]::ReadAllText($resolvedPath)
        }
        else {
            $jsonStringOrFilePath
        }
        try {
            $final = (ConvertFrom-Json -InputObject $newData -ErrorAction Stop)
        }
        catch {
            throw [VSError]::InvalidJsonInput($jsonStringOrFilePath)
        }
        $dict = [ordered]@{}
        $final.PSObject.Properties | ForEach-Object {
            $dict[$_.Name] = $_.Value
        }
        return [VSJson]::new($dict)
    }

    VSJson() : base() {}
    VSJson([IDictionary] $dictionary) {
        $dictionary.GetEnumerator() | ForEach-Object {
            $this[$_.Key] = $_.Value
        }
    }
    VSJson([psobject] $psObject) {
        $psObject.PSObject.Properties | ForEach-Object {
            $this[$_.Name] = $_.Value
        }
    }
    VSJson([string] $jsonStringOrFilePath) {
        $this = $this::Transform($jsonStringOrFilePath)
    }
    VSJson([string[]] $jsonStrings) {
        $newString = $jsonStrings -join [Environment]::NewLine
        $this = $this::Transform($newString)
    }
}

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 [VSYaml] Transform([string] $yamlStringOrFilePath) {
        $newData = if (Test-Path $yamlStringOrFilePath) {
            $resolvedPath = (Resolve-Path $yamlStringOrFilePath).Path
            [File]::ReadAllText($resolvedPath)
        }
        else {
            $yamlStringOrFilePath
        }
        try {
            $final = if (Get-Command cfn-flip* -ErrorAction SilentlyContinue) {
                $json = $newData | cfn-flip | Out-String
                ConvertFrom-Json -InputObject $json -ErrorAction Stop
            }
            else {
                ConvertFrom-Yaml -Yaml $newData -ErrorAction Stop
            }
        }
        catch {
            throw [VSError]::InvalidYamlInput($yamlStringOrFilePath)
        }
        $dict = [ordered]@{}
        $final.PSObject.Properties | ForEach-Object {
            $dict[$_.Name] = $_.Value
        }
        return [VSYaml]::new($dict)
    }

    VSYaml() : base() {}
    VSYaml([IDictionary] $dictionary) {
        $dictionary.GetEnumerator() | ForEach-Object {
            $this[$_.Key] = $_.Value
        }
    }
    VSYaml([psobject] $psObject) {
        $psObject.PSObject.Properties | ForEach-Object {
            $this[$_.Name] = $_.Value
        }
    }
    VSYaml([string] $yamlStringOrFilePath) {
        $this = $this::Transform($yamlStringOrFilePath)
    }
    VSYaml([string[]] $yamlStrings) {
        $newString = $yamlStrings -join [Environment]::NewLine
        $this = $this::Transform($newString)
    }
}

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) {}
}