src/scriptobject/ClassManager.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. # Define the class member variable used to emulate the native PowerShell class behavior with [ClassName]::StaticMethodName. # This may be required by other classes in the system implementation that rely on it, so those may need to be defined later. remove-variable -erroraction ignore ([ScriptClassSpecification]::Parameters.Language.ClassCollectionName) -force new-variable ([ScriptClassSpecification]::Parameters.Language.ClassCollectionName) -value ([PSCustomObject] @{([NativeObjectBuilder]::NativeTypeMemberName)=([ScriptClassSpecification]::Parameters.Language.ClassCollectionType)}) -option readonly -passthru # This class implements the type system as a whole, storing state about defined types and providing access to information # about them and the ability to instantiate instances of defined types. It is accessed as a singleton, though future # implementations could allow for multiple instances to exist; perhaps that could be used to model module-scoped # classes at some point. class ClassManager { ClassManager([PSModuleInfo] $targetModule) { # The targetModule argument is required so that the class collection variable can be created # at scope script of the correct module, i.e. the module that hosts this code. Otherwise PowerShell # tries to find the variable outside of module scope in the "global environment." $classCollectionVariable = . $targetModule.newboundscriptblock( {get-variable -scope script ([ScriptClassSpecification]::Parameters.Language.ClassCollectionName) } ) $this.classCollectionBuilder = [NativeObjectBuilder]::new($null, $classCollectionVariable.value, [NativeObjectBuilderMode]::Modify) } [ClassDefinition] DefineClass([string] $className, [ScriptBlock] $classBlock, [object[]] $classArguments) { $existingClass = $this.FindClassInfo($className) if ( $existingClass ) { if ( ! $this.allowRedefinition ) { throw "Class '$className' is already defined" } write-verbose "Class '$className' already exists, will attempt to redefine it." } $classBuilder = [ScriptClassBuilder]::new($className, $classBlock) $classInfo = $classBuilder.ToClassInfo($classArguments) $this.GeneralizeInstanceMethods($classInfo) $this.AddClass($classInfo) $visibleProperties = $classInfo.classDefinition.GetInstanceProperties() | where isSystem -eq $false | select -expandproperty name [NativeObjectBuilder]::RegisterClassType($className, $visibleProperties, $classInfo.prototype) return $classInfo.classDefinition } [object] CreateObject([string] $className, [object[]] $constructorArguments) { $classInfo = $this.GetClassInfo($className) $object = [NativeObjectBuilder]::CopyFrom($classInfo.prototype) $this.InitializeObject($object, $classInfo.classDefinition.constructor, $constructorArguments) return $object } [ClassInfo] GetClassInfo($className) { $classInfo = $this.FindClassInfo($className) if ( ! $classInfo ) { throw "class '$className' does not exist" } return $classInfo } [ClassInfo] FindClassInfo($className) { return $this.classes[$className] } [bool] IsClassType($object, [string] $classType) { $isOfType = $object -is [PSCustomObject] # Check for the native object type if ( $isOfType ) { # This is only a valid class if it has the required class member $isOfType = ($object | gm ([ScriptClassSpecification]::Parameters.Schema.ClassMember.Name) -erroraction ignore) # If it does have the member, validate it if ( $isOfType ) { $classMember = $object.$([ScriptClassSpecification]::Parameters.Schema.ClassMember.Name) $classMemberClassName = if ( $classMember ) { $classMember.$([ScriptClassSpecification]::Parameters.Schema.ClassMember.Structure.ClassNameMemberName) } # A null member is just a primitve type, but if it's # non-null it MUST be an actually defined class $isOfType = ($classMemberClassName -eq $null) -or $this.FindClassInfo($classMemberClassName) -ne $null # If the caller specified a type to validate against, # see if this object's typename matches the type # specified by the caller if ( $isOfType -and $classType ) { $objectTypeName = if ( $classMemberClassName ) { $classMemberClassName } else { # This is the type name for an object with # a null class member [ScriptClassSpecification]::Parameters.Schema.ClassMember.Type } $isOfType = $classType -eq $objectTypeName } } } return $isOfType } [void] SetClass([ClassInfo] $classInfo) { $this.GetClassInfo($classInfo.classDefinition.Name) | out-null $this.AddClass($classInfo) } hidden [void] AddClass([ClassInfo] $classInfo) { $className = $classInfo.classDefinition.Name $this.classes[$className] = $classInfo if ( $this.classCollectionBuilder ) { $classMemberName = [ScriptClassSpecification]::Parameters.Schema.ClassMember.Name $this.classCollectionBuilder.RemoveMember($className, 'ScriptProperty', $true) $this.classCollectionBuilder.AddMember($className, 'ScriptProperty', [ScriptBlock]::Create("[ClassManager]::Get().classes['$className'].prototype.$classMemberName"), $null) } } static [ClassManager] Get() { return [ClassManager]::singleton } static [void] RestoreMissingObjectMethods([ClassInfo] $classInfo, [PSCustomObject] $object, [bool] $staticContext) { $builder = [NativeObjectBuilder]::new($null, $object, [NativeObjectBuilderMode]::Modify) $methods = if ( $staticContext ) { $classInfo.classDefinition.GetStaticMethods() } else { $classInfo.classDefinition.GetInstanceMethods() } $methods | foreach { $builder.AddMethod($_.name, $_.block) } [ScriptClassBuilder]::commonMethods.GetEnumerator() | foreach { $builder.AddMethod($_.name, $_.value) } } static [void] Initialize([PSModuleInfo] $targetModule) { [ClassManager]::singleton = [ClassManager]::new($targetModule) [NativeObjectBuilder]::RegisterClassType([ScriptClassSpecification]::Parameters.Language.ClassCollectionType, @(), $null) } hidden [void] InitializeObject($object, $constructorBlock, [object[]] $constructorArguments) { if ( $constructorBlock ) { $object.InvokeScript($constructorBlock, $constructorArguments) | out-null } } hidden [void] GeneralizeInstanceMethods([ClassInfo] $classInfo) { $builder = [NativeObjectBuilder]::New($null, $classInfo.prototype, [NativeObjectBuilderMode]::Modify) $classInfo.classDefinition.GetInstanceMethods() | foreach { $generalizedBlock = $this.GetGeneralizedMethodBlock($_) $builder.RemoveMember($_.name, 'ScriptMethod', $false) $builder.AddMethod($_.name, $generalizedBlock) } } hidden [ScriptBlock] GetGeneralizedMethodBlock([Method] $method) { $block = [ScriptBlock]::Create($this::GeneralizedMethodTemplate -f $method.name) return $method.block.module.newboundscriptblock($block) } static [ClassManager] $singleton = $null hidden static [string] $GeneralizedMethodTemplate = @' $block = (get-scriptclass -Detailed $this.scriptclass.classname).classdefinition.instancemethods['{0}'].block . $block @args '@ $classCollectionBuilder = $null $allowRedefinition = $true $classes = @{} } [ClassManager]::Initialize({}.module) $mymanager = [ClassManager]::Get() |