pkg/modules/scriptclass/0.12.2/src/scriptclass.ps1
# Copyright 2017, 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. set-strictmode -version 2 set-alias const new-constant $__classTable = @{} if ( ! (test-path variable:stricttypecheckingtypename) ) { new-variable -name StrictTypeCheckingTypename -value '__scriptclass_strict_value__' -Option Readonly } if ( ! (test-path variable:scriptclasstypename) ) { new-variable -name ScriptClassTypeName -value 'ScriptClassType' -option Readonly } $:: = [PSCustomObject] @{} function __clear-typedata($className) { $existingTypeData = get-typedata $className if ($existingTypeData -ne $null) { $existingTypeData | remove-typedata } } __clear-typedata $scriptClassTypeName function add-scriptclass { param( [parameter(mandatory=$true)] [string] $className, [scriptblock] $classBlock ) $classData = @{TypeName=$className;MemberType='NoteProperty';DefaultDisplayPropertySet=@('PSTypeName');MemberName='PSTypeName';Value=$className;serializationmethod='SpecificProperties';PropertySerializationSet=@('PSTypeName')} try { $classDefinition = __new-class $classData __add-classmember $className $classDefinition $classBlock __define-class $classDefinition | out-null __remove-publishedclass $className $:: | add-member -name $className -memberType 'ScriptProperty' -value ([ScriptBlock]::Create("get-class '$className'")) } catch { __remove-publishedclass $className __clear-typedata $className __remove-class $className throw } } function new-scriptobject { [cmdletbinding()] param( [parameter(mandatory=$true)] [string] $className, [parameter(valuefromremainingarguments=$true)] $argumentlist ) $existingClass = __find-existingClass $className $newObject = [PSCustomObject] $existingClass.prototype.psobject.copy() __invoke-methodwithcontext $newObject '__initialize' @argumentlist | out-null $newObject } function get-class([string] $className) { $existingClass = __find-existingClass $className $existingClass.prototype.scriptclass } function test-scriptobject { [cmdletbinding()] param( [parameter(valuefrompipeline=$true, mandatory=$true)] $Object, $ScriptClass = $null ) $isClass = $false if ( $Object -is [PSCustomObject] ) { $objectClassName = try { $Object.scriptclass.classname } catch { $null } # Does the object's scriptclass object specify a valid type name and does its # PSTypeName match? $isClass = (__find-existingclass $objectClassName) -ne $null -and $Object.psobject.typenames.contains($objectClassName) if ($isClass -and $ScriptClass -ne $null) { # Now find the target type if it was specified -- map any string to # a class object $targetClass = if ( $ScriptClass -is [PSCustomObject] ) { $ScriptClass } elseif ($ScriptClass -is [string]) { get-class $ScriptClass } else { throw "Class must be specified as type [string] or type [PSCustomObject]" } # Now see if the type of the object's class matches the target class $isClass = $Object.psobject.typenames.contains($targetClass.ClassName) } } $isClass } function invoke-method($context, $do) { $action = $do $result = $null if ($context -eq $null) { throw "Invalid context -- context may not be `$null" } $object = $context if (! ($context -is [PSCustomObject])) { $object = [PSCustomObject] $context if (! ($context -is [PSCustomObject])) { throw "Specified context is not compatible with [PSCustomObject]" } } if ($action -is [string]) { $result = __invoke-methodwithcontext $object $action @args } elseif ($action -is [ScriptBlock]) { $result = __invoke-scriptwithcontext $object $action @args } else { throw "Invalid action type '$($action.gettype())'. Either a method name of type [string] or a scriptblock of type [ScriptBlock] must be supplied to 'with'" } $result } function =>($method) { if ($method -eq $null) { throw "A method must be specified" } $objects = @() $input | foreach { $objects += $_ } if ( $objects.length -lt 1) { throw "Pipeline must have at least 1 object for $($myinvocation.mycommand.name)" } $methodargs = $args $results = @() $objects | foreach { $results += (with $_ $method @methodargs) } if ( $results.length -eq 1) { $results[0] } else { $results } } function ::> { param( [parameter(valuefrompipeline=$true)] [string] $classSpec, [parameter(position=0)] $method, [parameter(valuefromremainingarguments=$true)] $remaining ) [cmdletbinding(positionalbinding=$false)] $classObject = get-class $classSpec $classObject |=> $method @remaining } function __new-class([Hashtable]$classData) { $className = $classData['Value'] # remove existing type data __clear-typedata $className Update-TypeData -force @classData $typeSystemData = get-typedata $classname $prototype = [PSCustomObject]@{PSTypeName=$className} $classDefinition = @{typedata=$typeSystemData;prototype=$prototype} $__classTable[$className] = $classDefinition $classDefinition } function __find-class($className) { $__classTable[$className] } function __find-existingClass($className) { $existingClass = __find-class $className if ($existingClass -eq $null) { throw "class '$className' not found" } $existingClass } function __remove-publishedclass($className) { try { $::.psobject.members.remove($className) } catch { } } function __remove-class($className) { $__classTable.Remove($className) } function __add-classmember($className, $classDefinition, $classBlock) { $classMember = [PSCustomObject] @{ PSTypeName = $ScriptClassTypeName ClassName = $className ClassScriptBlock = $classBlock InstanceMethods = @{} InstanceProperties = @{} TypedMembers = @{} ScriptClass = $null } __add-member $classMember PSTypeData ScriptProperty ([ScriptBlock]::Create("(__find-existingclass '$className').typedata")) __add-typemember NoteProperty $className 'ScriptClass' $null $classMember -hidden } function __invoke-methodwithcontext($object, $method) { $methodScript = try { $object.psobject.members[$method].script } catch { throw [Exception]::new("Failed to invoke method '$method' on object of type $($object.gettype())", $_.exception) } __invoke-scriptwithcontext $object $methodScript @args } function __invoke-scriptwithcontext($objectContext, $script) { $variables = [PSVariable[]]@() $thisVariable = [PSVariable]::new('this', $objectContext) $variables += $thisVariable try { $variables += get-variable pscmdlet 2>$null } catch { } $functions = @{} $objectContext.psobject.members | foreach { if ( $_.membertype -eq 'ScriptMethod' ) { $functions[$_.name] = $_.value.script } } $result = try { # Very strange -- an array of cardinality 1 generates an error when used in the method call to InvokeWithContext, so if there's only one element, convert it back to that one element if ($variables.length -eq 1 ) { $variables = $variables[0] } $invokeWrapper = { try { $__results = . $script @args @{ result = $__results succeeded = $true } } catch { @{ result = $_ succeeded = $false } } } $invokeWrapper.InvokeWithContext($functions, $variables, $args) } catch { write-error $_ get-pscallstack | write-error $_ } if ( $result.succeeded ) { $result.result } else { throw $result.result } } function __add-scriptpropertytyped($object, $memberName, $memberType, $initialValue = $null) { if ($initialValue -ne $null) { $evalString = "param(`[$memberType] `$value)" $evalBlock = [ScriptBlock]::Create($evalString) (. $evalBlock $initialValue) | out-null } $getBlock = [ScriptBlock]::Create("[$memberType] `$this.TypedMembers['$memberName']") $setBlock = [Scriptblock]::Create("param(`$val) `$this.TypedMembers['$memberName'] = [$memberType] `$val") __add-member $object $memberName 'ScriptProperty' $getBlock $null $setBlock $object.TypedMembers[$memberName] = $initialValue } function __add-member($prototype, $memberName, $psMemberType, $memberValue, $memberType = $null, $memberSecondValue = $null, $force = $false) { $arguments = @{name=$memberName;memberType=$psMemberType;value=$memberValue} if ($memberSecondValue -ne $null) { $arguments['secondValue'] = $memberSecondValue } $newMember = ($prototype | add-member -passthru @arguments) } function __add-typemember($memberType, $className, $memberName, $typeName, $initialValue, $constant = $false, [switch] $hiddenMember) { if ($typeName -ne $null -and -not $typeName -is [Type]) { throw "Invalid argument passed for type -- the argument must be of type [Type]" } $classDefinition = __find-class $className $memberExists = $classDefinition.typedata.members.keys -contains $memberName if ($memberName -eq $null ) { throw 'A $null member name was specified' } if ($memberExists) { throw "Member '$memberName' already exists for type '$className'" } $defaultDisplay = @() $propertySerializationSet = @() $classDefinition.typedata.defaultdisplaypropertyset.referencedproperties | foreach { $defaultDisplay += $_ } $classDefinition.typedata.propertyserializationset.referencedproperties | foreach { $propertyserializationset += $_ } if (! $hiddenMember.ispresent) { $defaultDisplay += $memberName if ($memberType -eq 'NoteProperty' -or $memberType -eq 'ScriptProperty') { $propertyserializationset += $memberName $classDefinition.prototype.scriptclass.instanceproperties[$memberName] = $typeName } } $aliasName = "__$($memberName)" $realName = $memberName if ($typeName -ne $null -or $constant) { $realName = $aliasName $aliasName = $memberName } $nameTypeData = @{TypeName=$className;MemberType=$memberType;MemberName=$realName;Value=$initialValue;defaultdisplaypropertyset=$defaultdisplay;propertyserializationset=$propertyserializationset;serializationmethod='SpecificProperties'} __add-member $classDefinition.prototype $realName $memberType $initialValue $typeName Update-TypeData -force @nameTypeData if ($typeName -ne $null) { # Check to make sure any initializer is compatible with the declared type if ($initialValue -ne $null) { $evalString = "param(`[$typeName] `$value)" $evalBlock = [ScriptBlock]::Create($evalString) (. $evalBlock $initialValue) | out-null } } if ($typeName -ne $null -or $constant) { $typeCoercion = if ( $typeName -ne $null ) { "[$typeName]" } else { '' } $getBlock = [ScriptBlock]::Create("$typeCoercion `$this.$realName") $setBlock = if (! $constant) { [Scriptblock]::Create("param(`$val) `$this.$realName = $typeCoercion `$val") } else { [Scriptblock]::Create("param(`$val) throw `"member '$aliasName' cannot be overwritten because it is read-only`"") } $aliasTypeData = @{TypeName=$className;MemberType='ScriptProperty';MemberName=$aliasName;Value=$getBlock;SecondValue=$setBlock} Update-TypeData -force @aliasTypeData } $typeSystemData = get-typedata $className $classDefinition.typeData = $typeSystemData } function __get-classmembers($classDefinition) { $__functions__ = ls function: $__variables__ = @{} $__classvariables__ = @{} function __initialize {} $script:__statics__ = @{} $script:__staticvars__ = @{} $scope = 0 $scopevariables = @{} $aftervariables = @{} # Due to some strange behaivor with dot-sourcing, variables # from the dot-sourced script are NOT always importing into scope 0. # In fact, they have been observed to import to scope 4!!! # Due to this, we need to enumerate ALL scopes before and after # sourcing the script and compare the results. We run the risk # of including scripts at global or script scope though, and may # need to do additional checks to avoid this. while ($scope -ge 0) { try { $scopevariables = get-variable -scope $scope } catch { $scope = -1 } if ($scope -ge 0) { $scopevariables | foreach { $__variables__[ $scope, $_.name -join ':' ] = $_ } $scope++ } } # Note that variables dot sourced here will not necessarily import at # scope 0 as one would think, so we'll need to retrieve all visible scopes . $classDefinition | out-null # Do NOT create new variables after this step to avoid retrieving them # and confusing them with new variables from the class. We'll need to filter out # the "_" and "PSItem" automatic variables -- those are never valid class member # names anyway. $scope = 0 $scopevariables = @{} while ($scope -ge 0) { try { $scopevariables = get-variable -scope $scope } catch { $scope = -1 } if ($scope -ge 0) { $scopevariables | foreach { if ($_.name -ne '_' -and $_.name -ne 'psitem' ) { $aftervariables[ $scope, $_.name -join ':' ] = $_ } } $scope++ } } $addedVariables = @{} $aftervariables.getenumerator() | foreach { if (! $__variables__.containskey($_.name)) { $varname = $_.value.name $varscope = [int32] (($_.name -split ':')[0]) $existingVariable = $addedVariables[$varname] if ($addedVariables[$varname] -eq $null -or $varscope -lt $addedVariables[$varname]) { $__classvariables__[$varname] = $_.value $addedVariables[$varname] = $varscope } } } $__classfunctions__ = @{} ls function: | foreach { $__classfunctions__[$_.name] = $_ } $__functions__ | foreach { if ( $_.scriptblock -eq $__classfunctions__[$_.name].scriptblock) { $__classfunctions__.remove($_.name) } } @{functions=$__classfunctions__;variables=$__classvariables__} } function strict-val { param( [parameter(mandatory=$true)] $type, $value = $null ) if (! $type -is [string] -and ! $type -is [Type]) { throw "The 'type' argument of type '$($type.gettype())' specified for strict-val must be of type [String] or [Type]" } $propType = if ( $type -is [Type] ) { $type } elseif ( $type.startswith('[') -and $type.endswith(']')) { iex $type } else { throw "Specified type '$propTypeName' was not of the form '[typename]'" } [PSCustomObject] @{ PSTypeName = $StrictTypeCheckingTypename; type = $propType; value = $value } } function new-constant { param( [parameter(mandatory=$true)] $name, [parameter(mandatory=$true)] $value ) $existingVariable = try { get-variable -name $name -scope 1 2> $null } catch { $null } if ( $existingVariable -eq $null ) { new-variable -name $name -value $value -scope 1 -option readonly *> (out-null) } elseif ($existingVariable.value -ne $value) { throw "Attempt to redefine constant '$name' from value '$($existingVariable.value) to '$value'" } } function __get-classproperties($memberData) { $classProperties = @{} $memberData.__newvariables__.getenumerator() | foreach { if ($classProperties.contains($_.name)) { throw "Attempted redefinition of property '$_.name'" } $propType = $null $propValSpec = $_.value.value $propVal = if ( $propValSpec -is [PSCustomObject] -and $propValSpec.psobject.typenames.contains($StrictTypeCheckingTypename) ) { $propType = $_.value.value.type $_.value.value.value } else { $_.value.value } $classProperties[$_.name] = @{type=$propType;value=$propVal;variable=$_.value} } $classProperties } function static([ScriptBlock] $staticBlock) { function static { throw "The 'static' function may not be used from within a static block" } $snapshot1 = ls function: $varsnapshot1 = get-variable -scope 0 . $staticBlock $snapshot2 = ls function: $varsnapshot2 = get-variable -scope 0 $delta = @{} $varDelta = @{} $snapshot2 | foreach { $delta[$_.name] = $_ } $snapshot1 | foreach { # For any function that exists in both snapshots, only remove if the # actual scriptblocks are the same. If they aren't, it just means # that a static function was defined with the same name as a non-static function, # and that's ok, since the static is essentially defined on the class and not the # object if ($delta[$_.name].scriptblock -eq $_.scriptblock) { $delta.remove($_.name) } } $varsnapshot2 | foreach { $varDelta[$_.name] = $_ } $varsnapshot1 | foreach { if ($varDelta[$_.name].gethashcode() -eq $_.gethashcode()) { $varDelta.remove($_.name) } } $delta.getenumerator() | foreach { $statics = $script:__statics__ $statics[$_.name] = $_.value } $varDelta.getenumerator() | foreach { $staticvars = $script:__staticvars__ $staticvars[$_.name] = $_.value } } function modulefunc { param($functions, $aliases, $className, $_classDefinition) set-strictmode -version 2 # necessary because strictmode gets reset when you execute in a new module # Add the functions explicitly at script scope to avoid issues with importing into an interactive shell $functions | foreach { new-item "function:script:$($_.name)" -value $_.scriptblock } $aliases | foreach { set-alias $_.name $_.resolvedcommandname }; $__exception__ = $null $__newfunctions__ = @{} $__newvariables__ = @{} try { $__classmembers__ = __get-classmembers $_classDefinition $__newfunctions__ = $__classmembers__.functions $__newvariables__ = $__classmembers__.variables } catch { $__exception__ = $_ } export-modulemember -variable __memberResult, __newfunctions__, __newvariables__, __exception__ -function $__newfunctions__.keys } $__instanceWrapperTemplate = @' invoke-method $this $this.scriptclass.instancemethods['{0}'] @args '@ function __define-class($classDefinition) { $aliases = @((get-item alias:with), (get-item 'alias:new-so'), (get-item alias:const)) pushd function: $functions = ls invoke-method, '=>', new-scriptobject, new-constant, __invoke-methodwithcontext, __invoke-scriptwithcontext, __get-classmembers, static popd $memberData = $null $classDefinitionException = $null try { $memberData = new-module -ascustomobject -scriptblock (gi function:modulefunc).scriptblock -argumentlist $functions, $aliases, $classDefinition.typeData.TypeName, $classDefinition.prototype.ScriptClass.ClassScriptBlock $classDefinitionException = $memberData.__exception__ } catch { $classDefinitionException = $_ } if ($classDefinitionException -ne $null) { $badClassData = get-typedata $classDefinition.typeData.TypeName $badClassData | remove-typedata throw $classDefinitionException } $classProperties = __get-classproperties $memberData $classProperties.getenumerator() | foreach { __add-typemember NoteProperty $classDefinition.typeData.TypeName $_.name $_.value.type $_.value.value ($_.value.variable.options -eq 'readonly') } $nextFunctions = $memberData.__newfunctions__ $nextFunctions.getenumerator() | foreach { if ($nextFunctions[$_.name] -is [System.Management.Automation.FunctionInfo] -and $functions -notcontains $_.name) { $methodBlockWrapper = [ScriptBlock]::Create($__instanceWrapperTemplate -f $_.name) __add-typemember ScriptMethod $classDefinition.typeData.TypeName $_.name $null $methodBlockWrapper $classDefinition.prototype.scriptclass.InstanceMethods[$_.name] = $_.value.scriptblock } } $script:__statics__.getenumerator() | foreach { __add-member $classDefinition.prototype.scriptclass $_.name ScriptMethod $_.value.scriptblock } $script:__staticvars__.getenumerator() | foreach { if ( $_.value.value -is [PSCustomObject] -and $_.value.value.psobject.typenames.contains($StrictTypeCheckingTypename) ) { __add-scriptpropertytyped $classDefinition.prototype.scriptclass $_.name $_.value.value.type $_.value.value.value } else { __add-member $classDefinition.prototype.scriptclass $_.name NoteProperty $_.value.value } } } |