├── .gitignore ├── LICENSE ├── README.md ├── build.ps1 ├── src ├── Legislator.psd1 ├── Legislator.psm1 ├── Private │ ├── Assert-Legislator.ps1 │ └── New-AssemblyBuilder.ps1 └── Public │ ├── event.ps1 │ ├── interface.ps1 │ ├── method.ps1 │ └── property.ps1 └── test └── Legislator.Tests.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode 4 | 5 | ### VisualStudioCode ### 6 | .vscode/* 7 | !.vscode/tasks.json 8 | !.vscode/launch.json 9 | *.code-workspace 10 | 11 | ### VisualStudioCode Patch ### 12 | # Ignore all local history of files 13 | .history 14 | .ionide 15 | 16 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode 17 | 18 | ### Exclude build artifacts 19 | publish 20 | publish/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mathias R. Jessen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Legislator 2 | Legislator is a simple .NET interface authoring tool written in PowerShell 3 | 4 | ## Background 5 | 6 | I've heard a number of powershell users ask for the ability to _define_ .NET interfaces at runtime, rather than just implementing them using the Classes feature introduced in PowerShell v5. I'm not sure how scalable this approach is for DSC, but other interesting use cases might exist. 7 | 8 | ## Installation 9 | 10 | ### From PowerShell Gallery 11 | 12 | Legislator is listed on PowerShell Gallery and can be installed with a single command: 13 | 14 | ```powershell 15 | Install-Module Legislator 16 | ``` 17 | 18 | ### Manual installation 19 | 20 | Copy the contents of `src` to a folder called `Legislator` in your module directory, e.g.: 21 | 22 | ```powershell 23 | $modulePath = "C:\Program Files\WindowsPowerShell\Modules" 24 | mkdir "$modulePath\Legislator" 25 | Copy-Item .\src\* -Destination "$modulePath\Legislator\" -Recurse 26 | ``` 27 | 28 | Import using `Import-Module` as you would any other module: 29 | 30 | ```powershell 31 | Import-Module Legislator 32 | ``` 33 | 34 | ## Syntax 35 | 36 | The chosen syntax attempts to balance the simplicity of interface definitions found in C#, including the type signature layout found in that language and the need to easily parse the syntactical elements as PowerShell functions (hence the property/method prefix keywords). 37 | 38 | ### Commands 39 | 40 | #### `interface` 41 | 42 | A Legislator-generated interface starts with the `interface` command. It takes two positional mandatory parameters - a name and a scriptblock containing the interface declaration: 43 | 44 | interface IName { 45 | 46 | } 47 | 48 | 49 | #### `property` 50 | Property declarations in Legislator look like implicit properties in C#, prefixed with keyword `property`. 51 | Thus, the following interface definition in Legislator: 52 | 53 | interface IPoint { 54 | property int X 55 | property int Y 56 | } 57 | 58 | is equivalent to the following interface definition in C#: 59 | 60 | interface IPoint 61 | { 62 | int X 63 | { 64 | get; 65 | set; 66 | } 67 | int Y 68 | { 69 | get; 70 | set; 71 | } 72 | } 73 | 74 | 75 | #### `method` 76 | 77 | This example: 78 | 79 | ```powershell 80 | interface IWell { 81 | method void DropCoin([Coin]) 82 | } 83 | ``` 84 | 85 | is equivalent to the following in C#: 86 | 87 | ```csharp 88 | interface IWell 89 | { 90 | void DropCoin(Coin c); 91 | } 92 | ``` 93 | 94 | #### `event` 95 | 96 | The following event declaration in Legislator: 97 | 98 | ```powershell 99 | interface ICar { 100 | event EventHandler[EventArgs] EngineStarted 101 | } 102 | ``` 103 | 104 | is equivalent of C#: 105 | 106 | ```csharp 107 | interface ICar 108 | { 109 | event EventHandler EngineStarted; 110 | } 111 | ``` 112 | 113 | #### Multiple interface 114 | 115 | Legislator also supports chaining multiple interfaces, via the `Implements` parameter. 116 | 117 | The following interface declaration in Legislator: 118 | 119 | ```powershell 120 | interface ICrappyCar { 121 | event EventHandler[EventArgs] EngineBroke 122 | } -Implements IDisposable 123 | ``` 124 | 125 | is equivalent to the following in C#: 126 | 127 | ```csharp 128 | interface ICrappyCar : IDisposable { 129 | event EventHandler EngineBroke 130 | } 131 | ``` 132 | 133 | ### Syntax notes 134 | 135 | Legislator currently supports `method`, `property` and `event` members. No plans to supporting index accessor syntax. 136 | 137 | Due to limited usefulness, access modifiers are also not supported, all generated interfaces default to Public. 138 | 139 | Parameter naming for methods is also not currently support. 140 | 141 | The `property` definition supports a ReadOnly option that omits declaration of a property setter: 142 | 143 | ```powershell 144 | interface ITest { 145 | property int MyProperty -Option ReadOnly 146 | } 147 | ``` 148 | 149 | is equivalent to the following C# with an explicit getter but no setter: 150 | 151 | ```csharp 152 | interface ITest { 153 | int MyProperty 154 | { 155 | get; 156 | } 157 | } 158 | ``` 159 | 160 | ## Example Usage 161 | 162 | The following example defines a (_very_) rudimentary Calculator interface, and uses it for flexible dependency injection: 163 | 164 | - [ICalculator](https://gist.github.com/IISResetMe/ce158e711ea0ed0d0fb4b69bf3701a41) 165 | 166 | See also this strategy pattern example implementing a Legislator-defined interface: 167 | 168 | - [IPasswordPolicy](https://github.com/IISResetMe/IPasswordPolicy) 169 | 170 | ## Contributing 171 | 172 | If you'd like to submit a bug or otherwise contribute to the development of Legislator (or just say hi) feel free to [raise an issue](https://github.com/IISResetMe/Legislator/issues/new) or [contact me on Twitter](https://twitter.com/IISResetMe) 173 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | mkdir $PSScriptRoot\publish -Force 2 | Copy-Item $PSScriptRoot\src\* $PSScriptRoot\publish\ -Force -Recurse -------------------------------------------------------------------------------- /src/Legislator.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'Legislator.psm1' 3 | ModuleVersion = '0.0.5' 4 | GUID = '5331baea-66ef-4a5c-9168-bd0a85fdaec2' 5 | Author = 'Mathias R. Jessen (@IISResetMe)' 6 | CompanyName = 'IISResetMe' 7 | Copyright = '(c) 2020 IISResetMe. All rights reserved.' 8 | Description = 'Legislator is a simple .NET interface authoring tool written in PowerShell' 9 | PowerShellVersion = '5.0' 10 | FunctionsToExport = 'interface','method','property','event' 11 | PrivateData = @{ 12 | PSData = @{ 13 | # Tags applied to this module. These help with module discovery in online galleries. 14 | # Tags = @() 15 | 16 | # A URL to the license for this module. 17 | LicenseUri = 'https://github.com/IISResetMe/Legislator/blob/trunk/LICENSE' 18 | 19 | # A URL to the main website for this project. 20 | ProjectUri = 'https://github.com/IISResetMe/Legislator' 21 | 22 | # A URL to an icon representing this module. 23 | # IconUri = '' 24 | 25 | # ReleaseNotes of this module 26 | # ReleaseNotes = '' 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Legislator.psm1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # Attempt to retrieve relevant script files 4 | $Classes = Get-ChildItem (Join-Path $PSScriptRoot Classes) -ErrorAction SilentlyContinue -Filter *.class.ps1 5 | $Public = Get-ChildItem (Join-Path $PSScriptRoot Public) -ErrorAction SilentlyContinue -Filter *.ps1 6 | $Private = Get-ChildItem (Join-Path $PSScriptRoot Private) -ErrorAction SilentlyContinue -Filter *.ps1 7 | 8 | # Classes on which other classes might depend, must be specified in order 9 | $ClassDependees = @() 10 | 11 | # Import classes on which others depend first 12 | foreach($classDependee in $ClassDependees) 13 | { 14 | Write-Verbose "Loading class '$classDependee'" 15 | try{ 16 | . (Join-Path (Join-Path $PSScriptRoot .\Classes) "$classDependee.class.ps1") 17 | } 18 | catch{ 19 | Write-Error -Message "Failed to import class $($classDependee): $_" 20 | } 21 | } 22 | 23 | # Import any remaining class files 24 | foreach($class in $Classes|Where-Object {($_.Name -replace '\.class\.ps1') -notin $ClassDependees}) 25 | { 26 | Write-Verbose "Loading class '$class'" 27 | try{ 28 | . $class.FullName 29 | } 30 | catch{ 31 | Write-Error -Message "Failed to import dependant class $($class.FullName): $_" 32 | } 33 | } 34 | 35 | # dot source the functions 36 | foreach($import in @($Public;$Private)) 37 | { 38 | Write-Verbose "Loading script '$import'" 39 | try{ 40 | . $import.FullName 41 | } 42 | catch{ 43 | Write-Error -Message "Failed to import function $($import.FullName): $_" 44 | } 45 | } 46 | 47 | # Export public functions 48 | Write-Verbose "Exporting public functions: $($Public.BaseName)" 49 | Export-ModuleMember -Function $Public.BaseName 50 | -------------------------------------------------------------------------------- /src/Private/Assert-Legislator.ps1: -------------------------------------------------------------------------------- 1 | function Assert-Legislator 2 | { 3 | param ( 4 | [string]$MemberType 5 | ) 6 | 7 | if (-not $Legislator) 8 | { 9 | throw "$MemberType only allowed in interface declarations" 10 | } 11 | } -------------------------------------------------------------------------------- /src/Private/New-AssemblyBuilder.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Reflection.Emit 2 | 3 | function New-AssemblyBuilder 4 | { 5 | param ( 6 | [AllowEmptyString()] 7 | [string]$Name 8 | ) 9 | 10 | $assemblyName = [AssemblyName]::new("${Name}$((New-Guid)-replace'\W')") 11 | $assemblyAccess = [AssemblyBuilderAccess]::Run 12 | 13 | if($PSVersionTable['PSEdition'] -eq 'Core') { 14 | return [AssemblyBuilder]::DefineDynamicAssembly($assemblyName, $assemblyAccess) 15 | } 16 | 17 | return [AppDomain]::CurrentDomain.DefineDynamicAssembly($assemblyName, $assemblyAccess) 18 | } -------------------------------------------------------------------------------- /src/Public/event.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Reflection 3 | using namespace System.Reflection.Emit 4 | 5 | function event { 6 | param( 7 | [Parameter(Mandatory = $true, Position = 0)] 8 | [string]$TypeName, 9 | 10 | [Parameter(Mandatory = $true, Position = 1)] 11 | [Alias('Name')] 12 | [string]$EventName, 13 | 14 | [Parameter(Mandatory = $false, Position = 2)] 15 | [string]$Option 16 | ) 17 | 18 | Assert-Legislator -MemberType event 19 | 20 | try{ 21 | $handlerType = [Type]$TypeName 22 | } 23 | catch { 24 | throw [Exception]::new('Unrecognized type name', $_) 25 | return 26 | } 27 | 28 | $eventBuilder = $Legislator.DefineEvent($EventName, [EventAttributes]::None, $handlerType); 29 | 30 | $eventMethodAttributes = @( 31 | 'Public', 'HideBySig', 'SpecialName', 'Abstract', 'Virtual', 'NewSlot' 32 | ) -as [MethodAttributes] 33 | 34 | $addMethod = . method -TypeName:'void' -Name:"add_$EventName" -Attributes:$eventMethodAttributes -ParameterTypes @( $HandlerType ) -PassThru:$true 35 | $addMethod.DefineParameter(1, [ParameterAttributes]::None, 'value') 36 | $eventBuilder.SetAddOnMethod($addMethod) 37 | 38 | $removeMethod = . method -TypeName:'void' -Name:"remove_$EventName" -Attributes:$eventMethodAttributes -ParameterTypes @( $HandlerType ) -PassThru:$true 39 | $removeMethod.DefineParameter(1, [ParameterAttributes]::None, 'value') 40 | $eventBuilder.SetRemoveOnMethod($removeMethod) 41 | } 42 | -------------------------------------------------------------------------------- /src/Public/interface.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Reflection 3 | using namespace System.Reflection.Emit 4 | 5 | function interface { 6 | param( 7 | [Parameter(Mandatory = $true, Position = 0)] 8 | [ValidatePattern('^[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}][\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\p{Cf}]*$')] 9 | [string]$Name, 10 | 11 | [Parameter(Mandatory = $true, Position = 1)] 12 | [ValidateScript({ 13 | -not ( 14 | @($_.Ast.FindAll({ 15 | param($AST) 16 | $AST -is [System.Management.Automation.Language.CommandAst] 17 | },$true) |ForEach-Object GetCommandName) |Where-Object { $_ -notin 'property','method','event' } 18 | ) -or $(throw 'Only properties and methods can be defined in an interface') 19 | })] 20 | [scriptblock]$Definition, 21 | 22 | [Parameter()] 23 | [ValidateScript({-not($_ |Where-Object{-not $_.IsInterface})})] 24 | [type[]]$Implements, 25 | 26 | [Parameter()] 27 | [switch]$PassThru = $false 28 | ) 29 | 30 | if($Name -cnotlike 'I*'){ 31 | Write-Warning -Message "Naming rule violaion: Missing prefix: 'I'" 32 | } 33 | 34 | $interfaceAttributes = @( 35 | 'Public','Interface','Abstract','AnsiClass','AutoLayout' 36 | ) -as [TypeAttributes] 37 | 38 | $assemblyBuilder = New-AssemblyBuilder -Name $Name 39 | $moduleBuilder = $assemblyBuilder.DefineDynamicModule("__psinterfacemodule_$assemblyName") 40 | 41 | $Legislator = $moduleBuilder.DefineType($Name, $interfaceAttributes) 42 | 43 | if($PSBoundParameters.ContainsKey('Implements')){ 44 | foreach($interfaceImpl in $Implements |Sort-Object -Property FullName -Unique){ 45 | try{ 46 | $Legislator.AddInterfaceImplementation($interfaceImpl) 47 | } 48 | catch{ 49 | throw 50 | return 51 | } 52 | } 53 | } 54 | 55 | try{ 56 | $null = . $Definition 57 | $finalType = $Legislator.CreateType() 58 | 59 | if($PassThru){ 60 | return $finalType 61 | } 62 | } 63 | catch{ 64 | throw 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Public/method.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Reflection 3 | using namespace System.Reflection.Emit 4 | 5 | function method { 6 | param( 7 | [Parameter(Mandatory = $true, Position = 0)] 8 | [string]$TypeName, 9 | 10 | [Parameter(Mandatory = $true, Position = 1)] 11 | [Alias('Name')] 12 | [string]$MethodName, 13 | 14 | [Parameter(Mandatory = $false, Position = 2)] 15 | [AllowEmptyCollection()] 16 | [Type[]]$ParameterTypes, 17 | 18 | [Parameter(DontShow)] 19 | [MethodAttributes]$Attributes, 20 | 21 | [Parameter(DontShow)] 22 | [switch]$PassThru 23 | ) 24 | 25 | Assert-Legislator -MemberType method 26 | 27 | try{ 28 | $ReturnType = [Type]$TypeName 29 | } 30 | catch { 31 | throw [Exception]::new('Unrecognized type name', $_.Exception) 32 | return 33 | } 34 | 35 | $interfaceMethodAttributes = @( 36 | 'Public', 'HideBySig', 'Abstract', 'Virtual', 'NewSlot' 37 | ) -as [MethodAttributes] 38 | 39 | $interfaceMethodAttributes = $interfaceMethodAttributes -bor $Attributes 40 | 41 | $method = $Legislator.DefineMethod($MethodName, $interfaceMethodAttributes, $ReturnType, $ParameterTypes) 42 | if($PassThru){ 43 | return $method 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Public/property.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Reflection 3 | using namespace System.Reflection.Emit 4 | 5 | function property { 6 | param( 7 | [Parameter(Mandatory = $true, Position = 0)] 8 | [string]$TypeName, 9 | 10 | [Parameter(Mandatory = $true, Position = 1)] 11 | [Alias('Name')] 12 | [string]$PropertyName, 13 | 14 | [Parameter(Mandatory = $false, Position = 2)] 15 | [ValidateSet('ReadOnly')] 16 | [string]$Option 17 | ) 18 | 19 | Assert-Legislator -MemberType property 20 | 21 | try{ 22 | $Type = [Type]$TypeName 23 | } 24 | catch { 25 | throw [Exception]::new('Unrecognized type name', $_.Exception) 26 | return 27 | } 28 | 29 | $property = $Legislator.DefineProperty($PropertyName, [PropertyAttributes]::HasDefault, [CallingConventions]::HasThis, $Type, $null) 30 | 31 | $propertyMethodAttributes = @( 32 | 'Public', 'HideBySig', 'SpecialName', 'Abstract', 'Virtual', 'NewSlot' 33 | ) -as [MethodAttributes] 34 | 35 | $getMethod = . method -TypeName:$Type.FullName -Name:"get_$PropertyName" -Attributes:$propertyMethodAttributes -PassThru:$true 36 | $property.SetGetMethod($getMethod) 37 | 38 | if($Option -ne 'ReadOnly') { 39 | $setMethod = . method -TypeName:"void" -Name:"set_$PropertyName" -Attributes:$propertyMethodAttributes -ParameterTypes @( $Type ) -PassThru:$true 40 | $null = $setMethod.DefineParameter(1, [ParameterAttributes]::None, 'value'); 41 | 42 | $property.SetSetMethod($setMethod) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/Legislator.Tests.ps1: -------------------------------------------------------------------------------- 1 | $ModuleManifestName = 'Legislator.psd1' 2 | $ModuleManifestPath = "$PSScriptRoot\..\src\$ModuleManifestName" 3 | 4 | if (!$SuppressImportModule) { 5 | Import-Module $ModuleManifestPath -Scope Global -Force 6 | } 7 | 8 | Describe 'Module Manifest Tests' { 9 | It 'Passes Test-ModuleManifest' { 10 | Test-ModuleManifest -Path $ModuleManifestPath 11 | $? | Should -BeTrue 12 | } 13 | } 14 | 15 | Describe 'interface' { 16 | It 'Accepts a name and a scriptblock' { 17 | { 18 | interface "ITest$((New-Guid) -replace '\W')" { 19 | method int IsEmpty ([string]) 20 | } 21 | } |Should -Not -Throw 22 | } 23 | 24 | It 'Emits no output by default' { 25 | interface "ITest$((New-Guid) -replace '\W')" { 26 | 27 | } |Should -BeNullOrEmpty 28 | } 29 | 30 | It 'Emits the resulting interface type when PassThru is present' { 31 | $iName = "ITest$((New-Guid) -replace '\W')" 32 | interface $iName { 33 | 34 | } -PassThru |Should -BeOfType System.Type 35 | } 36 | 37 | It 'Produces valid interface types' { 38 | $iName = "ITest$((New-Guid) -replace '\W')" 39 | $iType = interface $iName { 40 | property string A 41 | } -PassThru 42 | 43 | $iType |Should -BeOfType System.Type 44 | 45 | $iType.IsInterface |Should -BeTrue 46 | 47 | $iProp = $iType.GetProperty('A') 48 | 49 | $iProp.PropertyType.FullName |Should -Be System.String 50 | } 51 | } 52 | --------------------------------------------------------------------------------