src/scriptobject/type/ScriptClassBuilder.ps1

# Copyright 2019, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# The ScriptClassBuilder implements what is essentially a derived type of the general type. In particular,
# ScriptClassBuilder is where the notion of static methods is implemented for the type, and object structure
# supporting that along a level of reflection capability that distinguishes ScriptClass types is implemented
# here.
class ScriptClassBuilder : ClassBuilder {
    ScriptClassBuilder([string] $className, [ScriptBlock] $classblock) :
    base($className, $classBlock, [ScriptClassSpecification]::Parameters.Language.ConstructorName) {
    }

    ScriptClassBuilder([ClassDefinitionContext] $context) :
    base($context) {
    }

    [ClassInfo] ToClassInfo([object[]] $classArguments) {
        $classMemberParameters = [ScriptClassSpecification]::Parameters.Schema.ClassMember
        $staticTarget = [NativeObjectBuilder]::CopyFrom($this::classMemberPrototype)
        $this.AddSystemProperty($classMemberParameters.Name, $null, $staticTarget)

        foreach ( $methodname in $this::commonMethods.keys ) {
            $this.AddSystemMethod($methodName, $this::commonMethods[$methodName] )
        }

        $classInfo = ([ClassBuilder]$this).ToClassInfo($classArguments)

        $classInfo.classDefinition.CopyPrototype($true, $staticTarget)

        $definitionContext = ([ClassBuilder]$this).definitionContext

        $instanceModule = ([ClassBuilder]$this).definitionContext.module
        $staticModule = if ( $definitionContext ) {
            $definitionContext.staticModule
        }

        $staticTarget.$($classMemberParameters.Structure.ClassNameMemberName) = $this.className
        $staticTarget.$($classMemberParameters.Structure.ModuleMemberName) = $staticModule

        $this::AddStaticCommonMethods($instanceModule, $classInfo.prototype)
        $this::AddStaticCommonMethods($staticModule, $staticTarget)

        $excludedMethodNames = $this::commonMethods.keys
        $filteredClassDefinition = [ClassDefinition]::GetFilteredDefinition($classInfo.classDefinition, $excludedMethodNames, $null)

        return [ClassInfo]::new($filteredClassDefinition, $classInfo.prototype, $classInfo.module)
    }

    static [void] Initialize() {
        $schemaParameters = [ScriptClassSpecification]::Parameters.Schema.ClassMember

        $primitiveClassPropertyNames = @(
            $schemaParameters.Name
            $schemaParameters.Structure.ClassNameMemberName
            $schemaParameters.Structure.ModuleMemberName
        )

        $primitiveClassProperties = $primitiveClasspropertyNames | foreach {
            [Property]::new($_, $null, $false, $false, $false)
        }

        $primitiveClassDefinition = [ClassDefinition]::new(
            $null,
            @(),
            @(),
            $primitiveClassProperties,
            @(),
            $null
        )

        [ScriptClassBuilder]::classMemberPrototype = $primitiveClassDefinition.ToPrototype($false)
    }

    static [string] GetVerbosePreference() {
        return $script:ScriptClassVerbosePreference
    }

    static $classMemberPrototype = $null
    static $commonMethods = @{
        InvokeMethod = {
            param([string] $methodName, $arguments)
            if ( ! $methodName ) {
                throw [ArgumentException]::new("Method name argument was `$null or empty")
            }
            $method = ($this.psobject.methods | where name -eq $methodname)
            if ( ! $method ) {
                throw [System.Management.Automation.MethodInvocationException]::new("The method '$methodName' could not be found on the object")
            }

            $oldVerbosePreference = $VerbosePreference
            $VerbosePreference = [ScriptClassBuilder]::GetVerbosePreference()

            try {
                . $method.script @arguments
            } finally {
                $VerbosePreference = $oldVerbosePreference
            }
        }
        InvokeScript = {
            param([ScriptBlock] $script, $arguments)
            if ( ! $script ) {
                throw [ArgumentException]::new("Scriptblock argument argument was `$null or not specified")
            }
            # An interesting alternative is this, but evaluating a new closure AND getting a new scriptblock
            # seems excessive for a single method call -- perhaps system methods like this can be bound
            # when they are added to the object prototype:
            #
            # . $script.module.newboundscriptblock($script.GetNewClosure()) @arguments
            #
            $thisVariable = [PSVariable]::new('this', $this)
            $script.InvokeWithContext(@{}, [PSVariable[]] @($thisVariable), $arguments)
        }
        GetScriptObjectHashCode = {
            $this.psobject.members.GetHashCode()
        }
    }

    static [void] AddStaticCommonMethods($module, $staticPrototype) {
        $objectBuilder = [NativeObjectBuilder]::new($null, $staticPrototype, [NativeObjectBuilderMode]::Modify)
        foreach ( $methodname in [ScriptClassBuilder]::commonMethods.keys ) {
            $normalizedMethod = $module.newboundscriptblock([ScriptClassBuilder]::commonMethods[$methodName])
            $objectBuilder.AddMethod($methodName, $normalizedMethod)
        }
    }
}

[ScriptClassBuilder]::Initialize()