classes/BindZoneClass.psm1

using namespace System.IO
using namespace System.Collections.Generic
using module '.\BindRecordClass.psm1'

class BindZone:List[BindRecord] {

    [ValidateNotNullOrEmpty()]
    [string]$Origin

    BindZone() {}

    BindZone( [FileInfo]$FileInfo ) {

        $this.Import( $FileInfo )
        
    }

    BindZone( [string]$Origin ) {

        $this.Origin = $Origin

    }

    BindZone( [string[]]$Records ) {

        $this.__InitZone( $Records )
        
    }

    BindZone( [BindRecord[]]$Records ) {

        $this.__InitZone( $Records )
        
    }

    BindZone( [string]$Origin, [string[]]$Records ) {

        $this.Origin = $Origin
        $this.__InitZone( $Records )

    }

    BindZone( [string]$Origin, [BindRecord[]]$Records ) {

        $this.Origin = $Origin
        $this.__InitZone( $Records )

    }

    hidden [void] __InitZone( [BindRecord[]]$Records ) {

        $this.AddRange( $Records )

    }

    [string] ToString() {

        # figure out column widths
        $ColumnWidths = @{
            HostName    = ( [string[]]$this.HostName    | Measure-Object -Maximum -Property Length ).Maximum
            TimeToLive  = ( [string[]]$this.TimeToLive  | Measure-Object -Maximum -Property Length ).Maximum
            RecordClass = ( [string[]]$this.RecordClass | Measure-Object -Maximum -Property Length ).Maximum
            RecordType  = ( [string[]]$this.RecordType  | Measure-Object -Maximum -Property Length ).Maximum
            #RecordData = ( [string[]]$this.RecordData | Measure-Object -Maximum -Property Length ).Maximum
        }

        $Format = "{5}{0,-$($ColumnWidths.HostName)} {1,$($ColumnWidths.TimeToLive)} {2,-$($ColumnWidths.RecordClass)} {3,-$($ColumnWidths.RecordType)} {4}"

        $Return = [List[string]]::new()

        if ( $this.Origin ) {
            
            $Return.Add( '$ORIGIN ' + $this.Origin )

        }

        $this |
            ForEach-Object { $Return.Add( $_.ToString( $Format ) ) }

        return $Return -join "`r`n"
    
    }

    [void] Import( [FileInfo]$Path ) {

        $ZoneOrigin = $null
        $Records = Resolve-Path $Path | Get-Content

        if ( $Records[0] -match '^\$ORIGIN\s+(?<Origin>\S+)\s*$' ) {

            $ZoneOrigin = $Matches.Origin
            $Records = $Records | Select-Object -Skip 1

        }

        if ( $ZoneOrigin ) {

            $this.Origin = $ZoneOrigin

        } else {
            
            Write-Warning 'Zone did not include an $ORIGIN'

        }

        $this.__InitZone( $Records )
        
    }

    [void] Export( [string]$Path ) {

        if ( $Errors = $this.GetErrors() ) {

            $Errors | ForEach-Object { Write-Warning $_ }

            Write-Error 'Zone Errors Present' -ErrorAction Stop

        }

        $this.ToString() |
            Set-Content -Path $Path

    }

    [List[string]] GetErrors() {

        [List[string]]$Errors = @()

        if ( -not $this.Origin ) {

            $Errors.Add( 'Zone Error - Must contain an $ORIGIN' )

        }

        if ( $this[0].RecordType -ne 'SOA' ) {

            $Errors.Add( 'Zone Error - Must start with SOA record' )

        }

        if ( ( $this | Where-Object RecordType -eq 'SOA' ).Count -lt 1 ) {

            $Errors.Add( 'Zone Error - Missing SOA record' )

        }

        if ( ( $this | Where-Object RecordType -eq 'SOA' ).Count -gt 1 ) {

            $Errors.Add( 'Zone Error - Multiple SOA records' )

        }

        $this |
            Group-Object 'HostName' |
            Where-Object { ( $_.Group.RecordType.Where({ $_ -eq 'CNAME' -or $_ -eq 'TXT' }) | Select-Object -Unique ).Count -gt 1 } |
            ForEach-Object {

                $Errors.Add( "Zone Error - Hostname '$($_.Name)' has conflicting record types, records may not have both a CNAME and a TXT record" )

            }

        $this |
            Where-Object { $_.RecordType -eq 'TXT' -and $_.Enabled } |
            Group-Object 'HostName' |
            Where-Object { ( $_.Group.TimeToLive | Select-Object -Unique ).Count -gt 1 } |
            ForEach-Object {

                $Errors.Add( "Zone Error - One or more TXT records for hostname '$($_.Name)' with dissimilar TTL values, all values for the same hostname must be the same" )

            }

        $this | ForEach-Object {

            if ( $RecordErrors = $_.GetErrors() ) {

                $RecordIndex = $this.IndexOf( $_ )

                $RecordErrors |
                    ForEach-Object { $Errors.Add( "Record $RecordIndex - $_" ) }

            }

        }

        return $Errors
    
    }

}