From a42f91aec21841c890322d46621d437d7c96f5fb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 16:14:34 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat(docs):=20=E2=9C=A8=20add=20documentati?= =?UTF-8?q?on=20for=20`ConvertFrom-JsonToHashtable`=20cmdlet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduces a new help file for the `ConvertFrom-JsonToHashtable` cmdlet. - Provides detailed information on syntax, parameters, and examples. - Ensures compatibility with both PowerShell 5.1 and PowerShell 7+. --- Gatekeeper/Classes/FeatureFlag.ps1 | 3 +- Gatekeeper/Classes/Property.ps1 | 6 +- .../Public/ConvertFrom-JsonToHashtable.ps1 | 80 +++++++++++++++++++ docs/en-US/ConvertFrom-JsonToHashtable.md | 80 +++++++++++++++++++ tests/Test-Condition.tests.ps1 | 2 +- tests/Test-FeatureFlag.tests.ps1 | 2 +- 6 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 Gatekeeper/Public/ConvertFrom-JsonToHashtable.ps1 create mode 100644 docs/en-US/ConvertFrom-JsonToHashtable.md diff --git a/Gatekeeper/Classes/FeatureFlag.ps1 b/Gatekeeper/Classes/FeatureFlag.ps1 index f3d0376..b7c2f94 100644 --- a/Gatekeeper/Classes/FeatureFlag.ps1 +++ b/Gatekeeper/Classes/FeatureFlag.ps1 @@ -1,4 +1,5 @@ . $PSScriptRoot\..\Enums\Effect.ps1 +. $PSScriptRoot\..\Public\ConvertFrom-JsonToHashtable.ps1 enum Operator { Equals @@ -159,7 +160,7 @@ class FeatureFlag { # $json = Get-Content -Raw -Path 'd:\Gatekeeper\Gatekeeper\featureFlag.json' # $featureFlag = [FeatureFlag]::FromJson($json) static [FeatureFlag] FromJson([string]$json) { - $data = ConvertFrom-Json $json -AsHashtable + $data = ConvertFrom-JsonToHashtable -InputObject $json return [FeatureFlag]::new($data) } diff --git a/Gatekeeper/Classes/Property.ps1 b/Gatekeeper/Classes/Property.ps1 index 8d32c07..6f49a70 100644 --- a/Gatekeeper/Classes/Property.ps1 +++ b/Gatekeeper/Classes/Property.ps1 @@ -1,3 +1,5 @@ +. $PSScriptRoot\..\Public\ConvertFrom-JsonToHashtable.ps1 + class PropertyValidation { [int]$Minimum [int]$Maximum @@ -124,7 +126,7 @@ class PropertySet { if (-not $validProperties) { throw 'Properties file is not valid.' } - $json = Get-Content $FilePath -Raw | ConvertFrom-Json -AsHashtable + $json = Get-Content $FilePath -Raw | ConvertFrom-JsonToHashtable if ($json -isnot [hashtable]) { throw 'Failed to create hashtable from json file' } @@ -135,7 +137,7 @@ class PropertySet { } static [PropertySet] FromJson([string]$json) { - $data = $json | ConvertFrom-Json -AsHashtable + $data = ConvertFrom-JsonToHashtable -InputObject $json return [PropertySet]::new($data) } diff --git a/Gatekeeper/Public/ConvertFrom-JsonToHashtable.ps1 b/Gatekeeper/Public/ConvertFrom-JsonToHashtable.ps1 new file mode 100644 index 0000000..fb7bf14 --- /dev/null +++ b/Gatekeeper/Public/ConvertFrom-JsonToHashtable.ps1 @@ -0,0 +1,80 @@ +function ConvertFrom-JsonToHashtable { + <# + .SYNOPSIS + Converts JSON to a hashtable with PowerShell 5.1 compatibility. + + .DESCRIPTION + Provides a compatibility layer for converting JSON to hashtables that works + with both PowerShell 5.1 and PowerShell 7+. In PowerShell 7+, uses the native + -AsHashtable parameter. In PowerShell 5.1, manually converts PSCustomObject + to hashtable. + + .PARAMETER InputObject + The JSON string to convert or pipeline input from Get-Content. + + .EXAMPLE + $json = Get-Content -Path "file.json" -Raw | ConvertFrom-JsonToHashtable + + .EXAMPLE + $data = ConvertFrom-JsonToHashtable -InputObject '{"key":"value"}' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$InputObject + ) + + process { + # In PowerShell 7+, ConvertFrom-Json supports -AsHashtable + if ($PSVersionTable.PSVersion.Major -ge 7) { + return ($InputObject | ConvertFrom-Json -AsHashtable) + } + + # For PowerShell 5.1, we need to manually convert PSCustomObject to Hashtable + $jsonObject = $InputObject | ConvertFrom-Json + return ConvertTo-Hashtable -InputObject $jsonObject + } +} + +function ConvertTo-Hashtable { + <# + .SYNOPSIS + Recursively converts PSCustomObject to Hashtable. + + .DESCRIPTION + Helper function that recursively converts PSCustomObject instances to hashtables. + Used for PowerShell 5.1 compatibility where ConvertFrom-Json doesn't support -AsHashtable. + + .PARAMETER InputObject + The object to convert to a hashtable. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object]$InputObject + ) + + if ($null -eq $InputObject) { + return $null + } + + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { + $collection = @( + foreach ($item in $InputObject) { + ConvertTo-Hashtable -InputObject $item + } + ) + return $collection + } + + if ($InputObject -is [PSCustomObject]) { + $hashtable = @{} + foreach ($property in $InputObject.PSObject.Properties) { + $hashtable[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value + } + return $hashtable + } + + # Return primitive types as-is + return $InputObject +} diff --git a/docs/en-US/ConvertFrom-JsonToHashtable.md b/docs/en-US/ConvertFrom-JsonToHashtable.md new file mode 100644 index 0000000..efda820 --- /dev/null +++ b/docs/en-US/ConvertFrom-JsonToHashtable.md @@ -0,0 +1,80 @@ +--- +external help file: Gatekeeper-help.xml +Module Name: Gatekeeper +online version: +schema: 2.0.0 +--- + +# ConvertFrom-JsonToHashtable + +## SYNOPSIS +Converts JSON to a hashtable with PowerShell 5.1 compatibility. + +## SYNTAX + +``` +ConvertFrom-JsonToHashtable [-InputObject] [-ProgressAction ] [] +``` + +## DESCRIPTION +Provides a compatibility layer for converting JSON to hashtables that works +with both PowerShell 5.1 and PowerShell 7+. +In PowerShell 7+, uses the native +-AsHashtable parameter. +In PowerShell 5.1, manually converts PSCustomObject +to hashtable. + +## EXAMPLES + +### EXAMPLE 1 +``` +$json = Get-Content -Path "file.json" -Raw | ConvertFrom-JsonToHashtable +``` + +### EXAMPLE 2 +``` +$data = ConvertFrom-JsonToHashtable -InputObject '{"key":"value"}' +``` + +## PARAMETERS + +### -InputObject +The JSON string to convert or pipeline input from Get-Content. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS diff --git a/tests/Test-Condition.tests.ps1 b/tests/Test-Condition.tests.ps1 index 1880194..d2ecc0c 100644 --- a/tests/Test-Condition.tests.ps1 +++ b/tests/Test-Condition.tests.ps1 @@ -19,7 +19,7 @@ Describe 'Test-Condition' { IsCompliant = $true } # load feature flag - $json = Get-Content -Path "$PSScriptRoot\fixtures\Updawg.json" -Raw | ConvertFrom-Json -AsHashtable + $json = Get-Content -Path "$PSScriptRoot\fixtures\Updawg.json" -Raw | ConvertFrom-JsonToHashtable $script:rules = $json.Rules $script:testConditionSplat = @{ Context = $script:context diff --git a/tests/Test-FeatureFlag.tests.ps1 b/tests/Test-FeatureFlag.tests.ps1 index e96519b..c129fe8 100644 --- a/tests/Test-FeatureFlag.tests.ps1 +++ b/tests/Test-FeatureFlag.tests.ps1 @@ -19,6 +19,6 @@ Describe 'Test-FeatureFlag' { IsCompliant = $true } # load feature flag - $json = Get-Content -Path "$PSScriptRoot\fixtures\Updawg.json" -Raw | ConvertFrom-Json -AsHashtable + $json = Get-Content -Path "$PSScriptRoot\fixtures\Updawg.json" -Raw | ConvertFrom-JsonToHashtable } } From f06e907ddb5eba6125078b1a62ee034338342c1f Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Thu, 22 Jan 2026 20:54:36 -0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(logging):=20=E2=9C=A8=20update=20loggi?= =?UTF-8?q?ng=20script=20handling=20in=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactored logging script handling to support both file paths and inline script blocks. * Improved verbosity for better debugging and clarity in logging operations. * Adjusted script block creation for enhanced performance and reliability. --- Gatekeeper/Configuration.psd1 | 20 ++++------------- Gatekeeper/Public/Import-GatekeeperConfig.ps1 | 22 +++++++++++-------- docs/en-US/Get-PropertySet.md | 2 +- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Gatekeeper/Configuration.psd1 b/Gatekeeper/Configuration.psd1 index 32ef366..5729558 100644 --- a/Gatekeeper/Configuration.psd1 +++ b/Gatekeeper/Configuration.psd1 @@ -11,32 +11,20 @@ Allow = @{ # We leave this disabled by default to avoid cluttering the console Enabled = $false - Script = { - param($Rule) - Write-Host "✅ Rule [$($Rule.Name)] matched and is allowed." - } + Script = 'param($Rule); Write-Host "✅ Rule [$($Rule.Name)] matched and is allowed."' } Deny = @{ # We leave this disabled by default to avoid cluttering the console Enabled = $false - Script = { - param($Rule) - Write-Host "⛔ Rule [$($Rule.Name)] matched and is denied." - } + Script = 'param($Rule); Write-Host "⛔ Rule [$($Rule.Name)] matched and is denied."' } Warning = @{ Enabled = $true - Script = { - param($Rule) - Write-Warning "⚠️ Rule [$($Rule.Name)] matched." - } + Script = 'param($Rule); Write-Warning "⚠️ Rule [$($Rule.Name)] matched."' } Audit = @{ Enabled = $true - Script = { - param($Rule) - Write-Host "Audit: $($Rule.Name)" - } + Script = 'param($Rule); Write-Host "Audit: $($Rule.Name)"' } } } diff --git a/Gatekeeper/Public/Import-GatekeeperConfig.ps1 b/Gatekeeper/Public/Import-GatekeeperConfig.ps1 index 0cc53de..1ec860d 100644 --- a/Gatekeeper/Public/Import-GatekeeperConfig.ps1 +++ b/Gatekeeper/Public/Import-GatekeeperConfig.ps1 @@ -36,17 +36,21 @@ function Import-GatekeeperConfig { Write-Verbose "Logging level '$level' is disabled, skipping." continue } - # Handle if the script is file or script block - if ($script:GatekeeperConfiguration.Logging[$level].Script -is [string]) { - $scriptPath = $script:GatekeeperConfiguration.Logging[$level].Script - if (-not (Test-Path -Path $scriptPath)) { - throw "Logging script file not found: $scriptPath" + # Handle if the script is a file path or string scriptblock + $scriptContent = $script:GatekeeperConfiguration.Logging[$level].Script + if ($scriptContent -is [string]) { + # Check if it's a file path + if (Test-Path -Path $scriptContent -ErrorAction SilentlyContinue) { + Write-Verbose "Loading logging script from file: $scriptContent" + $script:GatekeeperLogging[$level] = [scriptblock]::Create((Get-Content -Path $scriptContent -Raw)) + } else { + # Treat it as a script string and convert to scriptblock + Write-Verbose "Converting string to script block for logging level: $level" + $script:GatekeeperLogging[$level] = [scriptblock]::Create($scriptContent) } - Write-Verbose "Loading logging script from file: $scriptPath" - $script:GatekeeperLogging[$level] = [scriptblock]::Create((Get-Content -Path $scriptPath -Raw)) - } elseif ($script:GatekeeperConfiguration.Logging[$level].Script -is [scriptblock]) { + } elseif ($scriptContent -is [scriptblock]) { Write-Verbose "Using inline script block for logging level: $level" - $script:GatekeeperLogging[$level] = $script:GatekeeperConfiguration.Logging[$level].Script + $script:GatekeeperLogging[$level] = $scriptContent } else { Write-Warning "No valid script found for logging level: $level" } diff --git a/docs/en-US/Get-PropertySet.md b/docs/en-US/Get-PropertySet.md index c1f0023..81950ce 100644 --- a/docs/en-US/Get-PropertySet.md +++ b/docs/en-US/Get-PropertySet.md @@ -39,7 +39,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 0 +Position: 1 Default value: None Accept pipeline input: False Accept wildcard characters: False From 53c03815a85baba07b3563729dc1a25de8573f3f Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Thu, 22 Jan 2026 20:58:13 -0800 Subject: [PATCH 3/6] chore(release): 0.2.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ Gatekeeper/Gatekeeper.psd1 | 36 ++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36210bc..c7e9eda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.2.0] 2026-01-22 + +### Added + +- Documentation for `ConvertFrom-JsonToHashtable` cmdlet with syntax, + parameters, and examples for PowerShell 5.1 and 7+ compatibility. +- CLAUDE.md project guidance document with module overview, architecture, + development commands, testing patterns, and key concepts. + +### Changed + +- Refactored logging script handling in configuration to support both file + paths and inline script blocks. +- Enhanced auditing functionality with detailed instructions for configuring + logging in Configuration.psd1. +- Improved logging configuration options and integrated cached configuration + in Import-GatekeeperConfig. +- Enhanced Test-FeatureFlag to execute logging scripts based on rule outcomes. +- Updated README with configuration details and logging behavior explanations. + ## [0.1.1] - `Read-FeatureFile` uses a new static method to read the file and set the diff --git a/Gatekeeper/Gatekeeper.psd1 b/Gatekeeper/Gatekeeper.psd1 index c947a7c..0967534 100644 --- a/Gatekeeper/Gatekeeper.psd1 +++ b/Gatekeeper/Gatekeeper.psd1 @@ -9,28 +9,28 @@ @{ # Script module or binary module file associated with this manifest. - RootModule = 'Gatekeeper.psm1' + RootModule = 'Gatekeeper.psm1' # Version number of this module. - ModuleVersion = '0.1.1' + ModuleVersion = '0.2.0' # Supported PSEditions # CompatiblePSEditions = @() # ID used to uniquely identify this module - GUID = '7c2fb6fe-e024-4c39-b687-84fbe7473e3f' + GUID = '7c2fb6fe-e024-4c39-b687-84fbe7473e3f' # Author of this module - Author = 'Gilbert Sanchez' + Author = 'Gilbert Sanchez' # Company or vendor of this module - CompanyName = 'Gilbert Sanchez' + CompanyName = 'Gilbert Sanchez' # Copyright statement for this module - Copyright = '(c) Gilbert Sanchez. All rights reserved.' + Copyright = '(c) Gilbert Sanchez. All rights reserved.' # Description of the functionality provided by this module - Description = 'Helps implement feature flags in your PowerShell projects.' + Description = 'Helps implement feature flags in your PowerShell projects.' # Minimum version of the PowerShell engine required by this module # PowerShellVersion = '' @@ -51,9 +51,9 @@ # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @( + RequiredModules = @( @{ - ModuleName = 'Configuration' + ModuleName = 'Configuration' ModuleVersion = '1.6.0' } ) @@ -62,7 +62,7 @@ # RequiredAssemblies = @() # Script files (.ps1) that are run in the caller's environment prior to importing this module. - ScriptsToProcess = @('Enums\Effect.ps1', 'Classes\Property.ps1', 'Classes\FeatureFlag.ps1') + ScriptsToProcess = @('Enums\Effect.ps1', 'Classes\Property.ps1', 'Classes\FeatureFlag.ps1') # Type files (.ps1xml) to be loaded when importing this module # TypesToProcess = @() @@ -77,13 +77,13 @@ FunctionsToExport = '*' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = '*' + CmdletsToExport = '*' # Variables to export from this module VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = '*' + AliasesToExport = '*' # DSC resources to export from this module # DscResourcesToExport = @() @@ -95,12 +95,12 @@ # FileList = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ + PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. - Tags = @( + Tags = @( 'PSEdition_Core', 'Windows', 'Linux', @@ -108,16 +108,16 @@ ) # A URL to the license for this module. - LicenseUri = 'https://github.com/HeyItsGilbert/Gatekeeper/blob/master/LICENSE' + LicenseUri = 'https://github.com/HeyItsGilbert/Gatekeeper/blob/master/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://github.com/HeyItsGilbert/Gatekeeper/' + ProjectUri = 'https://github.com/HeyItsGilbert/Gatekeeper/' # A URL to an icon representing this module. - IconUri = 'https://raw.githubusercontent.com/HeyItsGilbert/Gatekeeper/main/static/icon.png' + IconUri = 'https://raw.githubusercontent.com/HeyItsGilbert/Gatekeeper/main/static/icon.png' # ReleaseNotes of this module - ReleaseNotes = 'https://github.com/HeyItsGilbert/Gatekeeper/blob/master/CHANGELOG.md' + ReleaseNotes = 'https://github.com/HeyItsGilbert/Gatekeeper/blob/master/CHANGELOG.md' # Prerelease string of this module # Prerelease = '' From d80cf0c38ff74957b7a03157570bdc413f60f132 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Thu, 22 Jan 2026 21:08:07 -0800 Subject: [PATCH 4/6] =?UTF-8?q?chore(changelog):=20=E2=9C=A8=20add=20missi?= =?UTF-8?q?ng=20section=20for=20changed=20items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added a new "Changed" section under version 0.1.1 in the CHANGELOG.md to document updates. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e9eda..335f9f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.1.1] +### Changed + - `Read-FeatureFile` uses a new static method to read the file and set the FilePath. From 6cbe62e91e3140a208f0dd2f697daed53b53fe12 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Thu, 22 Jan 2026 23:01:11 -0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(tests):=20=F0=9F=90=9B=20remove=20dupli?= =?UTF-8?q?cate=20module=20removal=20for=20'Configuration'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ensures that the 'Configuration' module is removed from the session to prevent conflicts during testing. * Improves test reliability by ensuring a clean module state before running tests. --- tests/NewFiles.tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/NewFiles.tests.ps1 b/tests/NewFiles.tests.ps1 index 8e79c94..3cfc65d 100644 --- a/tests/NewFiles.tests.ps1 +++ b/tests/NewFiles.tests.ps1 @@ -9,6 +9,7 @@ BeforeDiscovery { # Get module commands # Remove all versions of the module from the session. Pester can't handle multiple versions. Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore + Get-Module 'Configuration' | Remove-Module -Force -ErrorAction Ignore Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop function global:GetFullPath { param( From 9a66614e54924b1281f9aaf13c531b0f22e9bcd8 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Thu, 22 Jan 2026 23:55:38 -0800 Subject: [PATCH 6/6] =?UTF-8?q?fix(tests):=20=F0=9F=90=9B=20ensure=20all?= =?UTF-8?q?=20module=20versions=20are=20removed=20before=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated `Get-Module` calls to include `-All` flag for proper module cleanup. * This change prevents potential conflicts with multiple module versions during testing. --- tests/NewFiles.tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/NewFiles.tests.ps1 b/tests/NewFiles.tests.ps1 index 3cfc65d..f779224 100644 --- a/tests/NewFiles.tests.ps1 +++ b/tests/NewFiles.tests.ps1 @@ -8,8 +8,8 @@ BeforeDiscovery { # Get module commands # Remove all versions of the module from the session. Pester can't handle multiple versions. - Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore - Get-Module 'Configuration' | Remove-Module -Force -ErrorAction Ignore + Get-Module $env:BHProjectName -All | Remove-Module -Force -ErrorAction Ignore + Get-Module 'Configuration' -All | Remove-Module -Force -ErrorAction Ignore Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop function global:GetFullPath { param(