Classes/Property.ps1

class PropertyValidation {
    [int]$Minimum
    [int]$Maximum
    [int]$MinLength
    [int]$MaxLength
    [string]$Pattern

    PropertyValidation([hashtable]$data) {
        if ($data.ContainsKey("Minimum")) { $this.Minimum = [int]$data.Minimum }
        if ($data.ContainsKey("Maximum")) { $this.Maximum = [int]$data.Maximum }
        if ($data.ContainsKey("MinLength")) { $this.MinLength = [int]$data.MinLength }
        if ($data.ContainsKey("MaxLength")) { $this.MaxLength = [int]$data.MaxLength }
        if ($data.ContainsKey("Pattern")) { $this.Pattern = $data.Pattern }
    }

    [hashtable] ToHashtable() {
        $data = @{
            Minimum = $this.Minimum
            Maximum = $this.Maximum
            MinLength = $this.MinLength
            MaxLength = $this.MaxLength
            Pattern = $this.Pattern
        }
        return $data
    }
}

class PropertyDefinition {
    [string]$Name
    [string]$Type
    [object[]]$Enum
    [PropertyValidation]$Validation

    PropertyDefinition([string]$name, [hashtable]$data) {
        $this.Name = $name
        $this.Type = $data.Type

        if ($data.ContainsKey("Enum")) {
            $this.Enum = $data.Enum
        }

        if ($data.ContainsKey("Validation")) {
            $this.Validation = [PropertyValidation]::new($data.Validation)
        }
    }

    [hashtable] ToHashtable() {
        $data = @{
            Type = $this.Type
        }
        if ($null -ne $this.Enum) {
            $data.Enum = $this.Enum
        }
        if ($null -ne $this.Validation) {
            $data.Validation = $this.Validation.ToHashtable()
        }
        return $data
    }

    [bool]Validate ($Value) {
        if ($null -eq $this.Validation) {
            return $True
        }
        switch ($this.Type) {
            "integer" {
                if ($null -ne $this.Validation.Minimum -and $Value -lt $this.Validation.Minimum) {
                    Write-Warning "Value for '$($this.Name)' ($Value) is less than minimum allowed ($($this.Validation.Minimum))"
                    return $false
                }
                if ($null -ne $this.Validation.Maximum -and $Value -gt $this.Validation.Maximum) {
                    Write-Warning "Value for '$($this.Name)' ($Value) is greater than maximum allowed ($($this.Validation.Maximum))"
                    return $false
                }
            }
            "string" {
                if ($null -ne $this.Validation.MinLength -and $Value.Length -lt $this.Validation.MinLength) {
                    Write-Warning "Value for '$($this.Name)' is shorter than MinLength ($($this.Validation.MinLength))"
                    return $false
                }
                if ($null -ne $this.Validation.MaxLength -and $Value.Length -gt $this.Validation.MaxLength) {
                    Write-Warning "Value for '$($this.Name)' is longer than MaxLength ($($this.Validation.MaxLength))"
                    return $false
                }
                if ($this.Validation.Pattern -and ($Value -notmatch $this.Validation.Pattern)) {
                    Write-Warning "Value for '$($this.Name)' does not match pattern '$($this.Validation.Pattern)'"
                    return $false
                }
            }
        }
        #
        return $true
    }
}

class PropertySet {
    # TODO: Should this class have more properties?
    [string]$Name
    # FilePath is used to save the PropertySet to a file.
    [string]$FilePath
    [hashtable]$Properties

    PropertySet($Name) {
        $this.Name = $Name
        $this.Properties = @{}
    }
    PropertySet([hashtable]$rawData) {
        $this.Properties = @{}
        foreach ($key in $rawData.Keys) {
            if ($key -eq '$schema') { continue }
            Write-Verbose "Saving key: $key"
            $this.Properties[$key] = [PropertyDefinition]::new($key, $rawData[$key])
        }
    }

    static [PropertySet] FromFile ([string]$FilePath) {
        if (-not (Test-Path -Path $FilePath)) {
            throw "File path given did not exist: $FilePath"
        }
        $testJsonSplat = @{
            Path = $FilePath
            SchemaFile = "$PSScriptRoot\..\Schemas\Properties.json"
        }
        $validProperties = Test-Json @testJsonSplat
        if (-not $validProperties) {
            throw 'Properties file is not valid.'
        }
        $json = Get-Content $FilePath -Raw | ConvertFrom-Json -AsHashtable
        if ($json -isnot [hashtable]) {
            throw 'Failed to create hashtable from json file'
        }
        $ps = [PropertySet]::new($json)
        $ps.FilePath = (Resolve-Path $FilePath).Path
        $ps.Name = $ps.FilePath.BaseName
        return $ps
    }

    static [PropertySet] FromJson([string]$json) {
        $data = $json | ConvertFrom-Json -AsHashtable
        return [PropertySet]::new($data)
    }

    [PropertySet]AddProperty([PropertyDefinition]$Property) {
        $this.Properties.Add($Property)
        return $this
    }

    [PropertyDefinition]GetProperty([string]$name) {
        return $this.Properties[$name]
    }

    [string[]]GetNames() {
        return $this.Properties.Keys
    }

    [boolean]ContainsKey($name) {
        return $this.Properties.ContainsKey($name)
    }
    [void]Save() {
        if ($null -eq $this.FilePath) {
            throw "No file path specified to save PropertySet."
        }
        Write-Verbose "Saving PropertySet to file: $($this.FilePath)"
        if (-not (Test-Path -Path (Split-Path -Path $this.FilePath -Parent))) {
            New-Item -ItemType Directory -Path (Split-Path -Path $this.FilePath -Parent) | Out-Null
        }
        # Convert the properties to a hashtable and then to JSON
        # Use -Depth 10 to ensure nested objects are fully serialized
        $hashtable = @{
            '$schema' = 'https://raw.githubusercontent.com/PowerShell/Gatekeeper/main/Schemas/Properties.json'
        }
        foreach ($property in $this.Properties.Keys) {
            $hashtable[$property] = $this.Properties[$property].ToHashtable()
        }
        # Convert to JSON with a depth of 10 to handle nested objects
        $json = $hashtable | ConvertTo-Json -Depth 10
        Set-Content -Path $this.FilePath -Value $json
    }
}

# Argument Transformer
class PropertySetTransformAttribute : System.Management.Automation.ArgumentTransformationAttribute {

    ## Override the abstract method "Transform". This is where the user
    ## provided value will be inspected and transformed if possible.
    [object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object] $inputData) {
        if ($null -eq $inputData) {
            return $(Read-PropertySet)
        }
        $item = switch ($inputData.GetType().FullName) {
            # Return the existing item if it's already a PropertySet
            'PropertySet' { $inputData }
            'System.Collections.Hashtable' {
                [PropertySet]::new($inputData)
            }
            default {
                throw "Cannot convert type to PropertySet: $($inputData.GetType().FullName)"
            }
        }
        return $item
    }

    [string] ToString() {
        return '[PropertySetTransformAttribute()]'
    }
}