Template-PSModule.psm1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidAssignmentToAutomaticVariable', 'IsWindows', Justification = 'IsWindows doesnt exist in PS5.1' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseDeclaredVarsMoreThanAssignments', 'IsWindows', Justification = 'IsWindows doesnt exist in PS5.1' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] [CmdletBinding()] param() $baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) $script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1" $script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } $scriptName = $script:PSModuleInfo.Name Write-Debug "[$scriptName] - Importing module" if ($PSEdition -eq 'Desktop') { $IsWindows = $true } #region Data importer Write-Debug "[$scriptName] - [data] - Processing folder" $dataFolder = (Join-Path $PSScriptRoot 'data') Write-Debug "[$scriptName] - [data] - [$dataFolder]" Get-ChildItem -Path "$dataFolder" -Recurse -Force -Include '*.psd1' -ErrorAction SilentlyContinue | ForEach-Object { Write-Debug "[$scriptName] - [data] - [$($_.BaseName)] - Importing" New-Variable -Name $_.BaseName -Value (Import-PowerShellDataFile -Path $_.FullName) -Force Write-Debug "[$scriptName] - [data] - [$($_.BaseName)] - Done" } Write-Debug "[$scriptName] - [data] - Done" #endregion Data importer #region [init] Write-Debug "[$scriptName] - [init] - Processing folder" #region [init] - [initializer] Write-Debug "[$scriptName] - [init] - [initializer] - Importing" Write-Verbose '-------------------------------' Write-Verbose '--- THIS IS AN INITIALIZER ---' Write-Verbose '-------------------------------' Write-Debug "[$scriptName] - [init] - [initializer] - Done" #endregion [init] - [initializer] Write-Debug "[$scriptName] - [init] - Done" #endregion [init] #region [classes] - [private] Write-Debug "[$scriptName] - [classes] - [private] - Processing folder" #region [classes] - [private] - [SecretWriter] Write-Debug "[$scriptName] - [classes] - [private] - [SecretWriter] - Importing" class SecretWriter { [string] $Alias [string] $Name [string] $Secret SecretWriter([string] $alias, [string] $name, [string] $secret) { $this.Alias = $alias $this.Name = $name $this.Secret = $secret } [string] GetAlias() { return $this.Alias } } Write-Debug "[$scriptName] - [classes] - [private] - [SecretWriter] - Done" #endregion [classes] - [private] - [SecretWriter] Write-Debug "[$scriptName] - [classes] - [private] - Done" #endregion [classes] - [private] #region [classes] - [public] Write-Debug "[$scriptName] - [classes] - [public] - Processing folder" #region [classes] - [public] - [Book] Write-Debug "[$scriptName] - [classes] - [public] - [Book] - Importing" class Book { # Class properties [string] $Title [string] $Author [string] $Synopsis [string] $Publisher [datetime] $PublishDate [int] $PageCount [string[]] $Tags # Default constructor Book() { $this.Init(@{}) } # Convenience constructor from hashtable Book([hashtable]$Properties) { $this.Init($Properties) } # Common constructor for title and author Book([string]$Title, [string]$Author) { $this.Init(@{Title = $Title; Author = $Author }) } # Shared initializer method [void] Init([hashtable]$Properties) { foreach ($Property in $Properties.Keys) { $this.$Property = $Properties.$Property } } # Method to calculate reading time as 2 minutes per page [timespan] GetReadingTime() { if ($this.PageCount -le 0) { throw 'Unable to determine reading time from page count.' } $Minutes = $this.PageCount * 2 return [timespan]::new(0, $Minutes, 0) } # Method to calculate how long ago a book was published [timespan] GetPublishedAge() { if ( $null -eq $this.PublishDate -or $this.PublishDate -eq [datetime]::MinValue ) { throw 'PublishDate not defined' } return (Get-Date) - $this.PublishDate } # Method to return a string representation of the book [string] ToString() { return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" } } class BookList { # Static property to hold the list of books static [System.Collections.Generic.List[Book]] $Books # Static method to initialize the list of books. Called in the other # static methods to avoid needing to explicit initialize the value. static [void] Initialize() { [BookList]::Initialize($false) } static [bool] Initialize([bool]$force) { if ([BookList]::Books.Count -gt 0 -and -not $force) { return $false } [BookList]::Books = [System.Collections.Generic.List[Book]]::new() return $true } # Ensure a book is valid for the list. static [void] Validate([book]$Book) { $Prefix = @( 'Book validation failed: Book must be defined with the Title,' 'Author, and PublishDate properties, but' ) -join ' ' if ($null -eq $Book) { throw "$Prefix was null" } if ([string]::IsNullOrEmpty($Book.Title)) { throw "$Prefix Title wasn't defined" } if ([string]::IsNullOrEmpty($Book.Author)) { throw "$Prefix Author wasn't defined" } if ([datetime]::MinValue -eq $Book.PublishDate) { throw "$Prefix PublishDate wasn't defined" } } # Static methods to manage the list of books. # Add a book if it's not already in the list. static [void] Add([Book]$Book) { [BookList]::Initialize() [BookList]::Validate($Book) if ([BookList]::Books.Contains($Book)) { throw "Book '$Book' already in list" } $FindPredicate = { param([Book]$b) $b.Title -eq $Book.Title -and $b.Author -eq $Book.Author -and $b.PublishDate -eq $Book.PublishDate }.GetNewClosure() if ([BookList]::Books.Find($FindPredicate)) { throw "Book '$Book' already in list" } [BookList]::Books.Add($Book) } # Clear the list of books. static [void] Clear() { [BookList]::Initialize() [BookList]::Books.Clear() } # Find a specific book using a filtering scriptblock. static [Book] Find([scriptblock]$Predicate) { [BookList]::Initialize() return [BookList]::Books.Find($Predicate) } # Find every book matching the filtering scriptblock. static [Book[]] FindAll([scriptblock]$Predicate) { [BookList]::Initialize() return [BookList]::Books.FindAll($Predicate) } # Remove a specific book. static [void] Remove([Book]$Book) { [BookList]::Initialize() [BookList]::Books.Remove($Book) } # Remove a book by property value. static [void] RemoveBy([string]$Property, [string]$Value) { [BookList]::Initialize() $Index = [BookList]::Books.FindIndex({ param($b) $b.$Property -eq $Value }.GetNewClosure()) if ($Index -ge 0) { [BookList]::Books.RemoveAt($Index) } } } enum Binding { Hardcover Paperback EBook } enum Genre { Mystery Thriller Romance ScienceFiction Fantasy Horror } Write-Debug "[$scriptName] - [classes] - [public] - [Book] - Done" #endregion [classes] - [public] - [Book] Write-Debug "[$scriptName] - [classes] - [public] - Done" #endregion [classes] - [public] #region [functions] - [private] Write-Debug "[$scriptName] - [functions] - [private] - Processing folder" #region [functions] - [private] - [Get-InternalPSModule] Write-Debug "[$scriptName] - [functions] - [private] - [Get-InternalPSModule] - Importing" function Get-InternalPSModule { <# .SYNOPSIS Performs tests on a module. .EXAMPLE Test-PSModule -Name 'World' "Hello, World!" #> [CmdletBinding()] param ( # Name of the person to greet. [Parameter(Mandatory)] [string] $Name ) Write-Output "Hello, $Name!" } Write-Debug "[$scriptName] - [functions] - [private] - [Get-InternalPSModule] - Done" #endregion [functions] - [private] - [Get-InternalPSModule] #region [functions] - [private] - [Set-InternalPSModule] Write-Debug "[$scriptName] - [functions] - [private] - [Set-InternalPSModule] - Importing" function Set-InternalPSModule { <# .SYNOPSIS Performs tests on a module. .EXAMPLE Test-PSModule -Name 'World' "Hello, World!" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', Justification = 'Reason for suppressing' )] [CmdletBinding()] param ( # Name of the person to greet. [Parameter(Mandatory)] [string] $Name ) Write-Output "Hello, $Name!" } Write-Debug "[$scriptName] - [functions] - [private] - [Set-InternalPSModule] - Done" #endregion [functions] - [private] - [Set-InternalPSModule] Write-Debug "[$scriptName] - [functions] - [private] - Done" #endregion [functions] - [private] #region [functions] - [public] Write-Debug "[$scriptName] - [functions] - [public] - Processing folder" #region [functions] - [public] - [Get-PSModuleTest] Write-Debug "[$scriptName] - [functions] - [public] - [Get-PSModuleTest] - Importing" #Requires -Modules Utilities function Get-PSModuleTest { <# .SYNOPSIS Performs tests on a module. .EXAMPLE Test-PSModule -Name 'World' "Hello, World!" #> [CmdletBinding()] param ( # Name of the person to greet. [Parameter(Mandatory)] [string] $Name ) Write-Output "Hello, $Name!" } Write-Debug "[$scriptName] - [functions] - [public] - [Get-PSModuleTest] - Done" #endregion [functions] - [public] - [Get-PSModuleTest] #region [functions] - [public] - [New-PSModuleTest] Write-Debug "[$scriptName] - [functions] - [public] - [New-PSModuleTest] - Importing" #Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.0'} function New-PSModuleTest { <# .SYNOPSIS Performs tests on a module. .EXAMPLE Test-PSModule -Name 'World' "Hello, World!" .NOTES Testing if a module can have a [Markdown based link](https://example.com). !"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.," \[This is a test\] #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', Justification = 'Reason for suppressing' )] [Alias('New-PSModuleTestAlias1')] [Alias('New-PSModuleTestAlias2')] [CmdletBinding()] param ( # Name of the person to greet. [Parameter(Mandatory)] [string] $Name ) Write-Output "Hello, $Name!" } New-Alias New-PSModuleTestAlias3 New-PSModuleTest New-Alias -Name New-PSModuleTestAlias4 -Value New-PSModuleTest Set-Alias New-PSModuleTestAlias5 New-PSModuleTest Write-Debug "[$scriptName] - [functions] - [public] - [New-PSModuleTest] - Done" #endregion [functions] - [public] - [New-PSModuleTest] #region [functions] - [public] - [Set-PSModuleTest] Write-Debug "[$scriptName] - [functions] - [public] - [Set-PSModuleTest] - Importing" function Set-PSModuleTest { <# .SYNOPSIS Performs tests on a module. .EXAMPLE Test-PSModule -Name 'World' "Hello, World!" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', Justification = 'Reason for suppressing' )] [CmdletBinding()] param ( # Name of the person to greet. [Parameter(Mandatory)] [string] $Name ) Write-Output "Hello, $Name!" } Write-Debug "[$scriptName] - [functions] - [public] - [Set-PSModuleTest] - Done" #endregion [functions] - [public] - [Set-PSModuleTest] #region [functions] - [public] - [Test-PSModuleTest] Write-Debug "[$scriptName] - [functions] - [public] - [Test-PSModuleTest] - Importing" function Test-PSModuleTest { <# .SYNOPSIS Performs tests on a module. .EXAMPLE Test-PSModule -Name 'World' "Hello, World!" #> [CmdletBinding()] param ( # Name of the person to greet. [Parameter(Mandatory)] [string] $Name ) Write-Output "Hello, $Name!" } Write-Debug "[$scriptName] - [functions] - [public] - [Test-PSModuleTest] - Done" #endregion [functions] - [public] - [Test-PSModuleTest] Write-Debug "[$scriptName] - [functions] - [public] - Done" #endregion [functions] - [public] #region [variables] - [private] Write-Debug "[$scriptName] - [variables] - [private] - Processing folder" #region [variables] - [private] - [PrivateVariables] Write-Debug "[$scriptName] - [variables] - [private] - [PrivateVariables] - Importing" $script:HabitablePlanets = @( @{ Name = 'Earth' Mass = 5.97 Diameter = 12756 DayLength = 24.0 }, @{ Name = 'Mars' Mass = 0.642 Diameter = 6792 DayLength = 24.7 }, @{ Name = 'Proxima Centauri b' Mass = 1.17 Diameter = 11449 DayLength = 5.15 }, @{ Name = 'Kepler-442b' Mass = 2.34 Diameter = 11349 DayLength = 5.7 }, @{ Name = 'Kepler-452b' Mass = 5.0 Diameter = 17340 DayLength = 20.0 } ) $script:InhabitedPlanets = @( @{ Name = 'Earth' Mass = 5.97 Diameter = 12756 DayLength = 24.0 }, @{ Name = 'Mars' Mass = 0.642 Diameter = 6792 DayLength = 24.7 } ) Write-Debug "[$scriptName] - [variables] - [private] - [PrivateVariables] - Done" #endregion [variables] - [private] - [PrivateVariables] Write-Debug "[$scriptName] - [variables] - [private] - Done" #endregion [variables] - [private] #region [variables] - [public] Write-Debug "[$scriptName] - [variables] - [public] - Processing folder" #region [variables] - [public] - [Moons] Write-Debug "[$scriptName] - [variables] - [public] - [Moons] - Importing" $script:Moons = @( @{ Planet = 'Earth' Name = 'Moon' } ) Write-Debug "[$scriptName] - [variables] - [public] - [Moons] - Done" #endregion [variables] - [public] - [Moons] #region [variables] - [public] - [Planets] Write-Debug "[$scriptName] - [variables] - [public] - [Planets] - Importing" $script:Planets = @( @{ Name = 'Mercury' Mass = 0.330 Diameter = 4879 DayLength = 4222.6 }, @{ Name = 'Venus' Mass = 4.87 Diameter = 12104 DayLength = 2802.0 }, @{ Name = 'Earth' Mass = 5.97 Diameter = 12756 DayLength = 24.0 } ) Write-Debug "[$scriptName] - [variables] - [public] - [Planets] - Done" #endregion [variables] - [public] - [Planets] #region [variables] - [public] - [SolarSystems] Write-Debug "[$scriptName] - [variables] - [public] - [SolarSystems] - Importing" $script:SolarSystems = @( @{ Name = 'Solar System' Planets = $script:Planets Moons = $script:Moons }, @{ Name = 'Alpha Centauri' Planets = @() Moons = @() }, @{ Name = 'Sirius' Planets = @() Moons = @() } ) Write-Debug "[$scriptName] - [variables] - [public] - [SolarSystems] - Done" #endregion [variables] - [public] - [SolarSystems] Write-Debug "[$scriptName] - [variables] - [public] - Done" #endregion [variables] - [public] #region [finally] Write-Debug "[$scriptName] - [finally] - Importing" Write-Verbose '------------------------------' Write-Verbose '--- THIS IS A LAST LOADER ---' Write-Verbose '------------------------------' Write-Debug "[$scriptName] - [finally] - Done" #endregion [finally] #region Class exporter # Get the internal TypeAccelerators class to use its static methods. $TypeAcceleratorsClass = [psobject].Assembly.GetType( 'System.Management.Automation.TypeAccelerators' ) # Ensure none of the types would clobber an existing type accelerator. # If a type accelerator with the same name exists, throw an exception. $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get # Define the types to export with type accelerators. $ExportableEnums = @( [Binding] [Genre] ) $ExportableEnums | Foreach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." } foreach ($Type in $ExportableEnums) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { Write-Verbose "Enum already exists [$($Type.FullName)]. Skipping." } else { Write-Verbose "Importing enum '$Type'." $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } $ExportableClasses = @( [Book] [BookList] ) $ExportableClasses | Foreach-Object { Write-Verbose "Exporting class '$($_.FullName)'." } foreach ($Type in $ExportableClasses) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { Write-Verbose "Class already exists [$($Type.FullName)]. Skipping." } else { Write-Verbose "Importing class '$Type'." $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in ($ExportableEnums + $ExportableClasses)) { $null = $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure() #endregion Class exporter #region Member exporter $exports = @{ Alias = '*' Cmdlet = '' Function = @( 'Get-PSModuleTest' 'New-PSModuleTest' 'Set-PSModuleTest' 'Test-PSModuleTest' ) Variable = @( 'Moons' 'Planets' 'SolarSystems' ) } Export-ModuleMember @exports #endregion Member exporter |