From c8720978d4c97e7cffe4bf068d50a61ce41cb8a5 Mon Sep 17 00:00:00 2001 From: kormic911 Date: Tue, 23 Sep 2025 22:59:05 -0500 Subject: [PATCH 1/5] Adding some icons to the readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 4386b44..b26b118 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # PSFluentObjectValidation: Complete Implementation Guide +| GitHub Actions | PS Gallery | License | +|-------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|--------------------------------------| +| [![GitHub Actions Status][github-actions-badge]][github-actions-build] [![GitHub Actions Status][github-actions-badge-publish]][github-actions-build] | [![PowerShell Gallery][psgallery-badge]][psgallery] | [![License][license-badge]][license] | + ## General Overview The PSFluentObjectValidation represents a comprehensive evolution from simple property validation to a high-performance, feature-rich validation engine with advanced array indexing capabilities. @@ -264,3 +268,11 @@ Based on 10,000 iterations with corrected V3-compatible scenario testing: | **Array + Validation** | ❌ | ❌ | ❌ | ✅ | `users[*].email!` (all non-empty) | | **Deep Array Access** | ❌ | ❌ | ❌ | ✅ | `products[0].category.name` | | **Error Handling** | Basic | Basic | Enhanced | Advanced | Bounds checking, null safety | + +[github-actions-badge]: https://github.com/pwshdevs/PSFluentObjectValidation/actions/workflows/CI.yml/badge.svg +[github-actions-badge-publish]: https://github.com/pwshdevs/PSFluentObjectValidation/actions/workflows/publish.yaml/badge.svg +[github-actions-build]: https://github.com/pwshdevs/PSFluentObjectValidation/actions +[psgallery-badge]: https://img.shields.io/powershellgallery/dt/PSFluentObjectValidation.svg +[psgallery]: https://www.powershellgallery.com/packages/PSFluentObjectValidation +[license-badge]: https://img.shields.io/github/license/pwshdevs/PSFluentObjectValidation.svg +[license]: https://raw.githubusercontent.com/pwshdevs/PSFluentObjectValidation/main/LICENSE From d280bf7ab9d2002bfe5ae0f11d722ab520d8d02a Mon Sep 17 00:00:00 2001 From: kormic911 Date: Tue, 23 Sep 2025 23:00:03 -0500 Subject: [PATCH 2/5] fixing typo in readme url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b26b118..d9bf0c3 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ Based on 10,000 iterations with corrected V3-compatible scenario testing: | **Deep Array Access** | ❌ | ❌ | ❌ | ✅ | `products[0].category.name` | | **Error Handling** | Basic | Basic | Enhanced | Advanced | Bounds checking, null safety | -[github-actions-badge]: https://github.com/pwshdevs/PSFluentObjectValidation/actions/workflows/CI.yml/badge.svg +[github-actions-badge]: https://github.com/pwshdevs/PSFluentObjectValidation/actions/workflows/CI.yaml/badge.svg [github-actions-badge-publish]: https://github.com/pwshdevs/PSFluentObjectValidation/actions/workflows/publish.yaml/badge.svg [github-actions-build]: https://github.com/pwshdevs/PSFluentObjectValidation/actions [psgallery-badge]: https://img.shields.io/powershellgallery/dt/PSFluentObjectValidation.svg From 06ba4389bbf9138e2f57f358aff952418d7603ea Mon Sep 17 00:00:00 2001 From: kormic911 Date: Wed, 24 Sep 2025 00:35:08 -0500 Subject: [PATCH 3/5] Fixing typo in assert-exists Key Alias --- PSFluentObjectValidation/Public/Assert-Exist.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSFluentObjectValidation/Public/Assert-Exist.ps1 b/PSFluentObjectValidation/Public/Assert-Exist.ps1 index e634d39..b9abc49 100644 --- a/PSFluentObjectValidation/Public/Assert-Exist.ps1 +++ b/PSFluentObjectValidation/Public/Assert-Exist.ps1 @@ -4,7 +4,7 @@ function Assert-Exist { [Alias('In')] $InputObject, [Parameter(Mandatory=$true, ValueFromPipeline = $true)] - [Alias('Width', 'Test')] + [Alias('With', 'Test')] [string]$Key ) From 1a8a4a34e8e8e77ff171d242e2c79fa9a0c74b27 Mon Sep 17 00:00:00 2001 From: kormic911 Date: Wed, 24 Sep 2025 21:06:14 -0500 Subject: [PATCH 4/5] Adding test cases and updating the core c# processor to properly handle multi level array validation --- .github/copilot-instructions.md | 102 ++++ .vscode/tasks.json | 2 +- CHANGELOG.md | 26 +- .../PSFluentObjectValidation.psd1 | 8 +- .../Private/PSFluentObjectValidation.ps1 | 115 +++- .../Public/Assert-Exist.ps1 | 36 ++ .../Public/Test-Exist.ps1 | 36 ++ README.md | 196 +++---- docs/en-US/Assert-Exist.md | 111 ++++ docs/en-US/Test-Exist.md | 111 ++++ tests/PSFluentObjectValidation.tests.ps1 | 505 ++++++++++++++++++ 11 files changed, 1110 insertions(+), 138 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 docs/en-US/Assert-Exist.md create mode 100644 docs/en-US/Test-Exist.md create mode 100644 tests/PSFluentObjectValidation.tests.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..a45d404 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,102 @@ +# Copilot Instructions for PSFluentObjectValidation + +## Project Overview +PSFluentObjectValidation is a PowerShell module that provides fluent syntax for validating complex object structures using dot notation with validation operators. The core functionality is implemented as a C# class embedded in PowerShell, supporting deep object traversal, array indexing, and wildcard validation. + +## Architecture + +### Core Components +- **C# Implementation**: `PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1` contains the main logic as embedded C# code using `Add-Type` +- **Public Functions**: Thin PowerShell wrappers around the C# class + - `Test-Exist`: Safe validation that returns boolean + - `Assert-Exist`: Throws exceptions with detailed error messages +- **Module Structure**: Standard PowerShell module with Public/Private folder separation + +### Key Design Patterns + +#### Validation Syntax +The module uses a fluent dot notation with special operators: +- `property.nested` - Basic navigation +- `property!` - Non-empty validation (rejects null/empty/whitespace) +- `property?` - Existence validation (allows null values) +- `array[0]` - Array indexing +- `array[*]` - Wildcard validation (all elements must pass) + +#### Error Handling Strategy +- `Test-Exist` wraps `Assert-Exist` in try/catch, never throws +- `Assert-Exist` provides detailed error messages with context +- C# implementation uses regex patterns for parsing validation operators + +## Development Workflows + +### Build System (psake + PowerShellBuild) +```powershell +# Bootstrap dependencies first (one-time setup) +./build.ps1 -Bootstrap + +# Standard development workflow +./build.ps1 -Task Build # Compiles and validates module +./build.ps1 -Task Test # Runs Pester tests + analysis +./build.ps1 -Task Clean # Cleans output directory +``` + +The build uses PowerShellBuild tasks defined in `psakeFile.ps1`. The `requirements.psd1` manages all build dependencies including Pester 5.4.0, PSScriptAnalyzer, and psake. + +### Testing Strategy +Tests live in `tests/` directory following these patterns: +- `Manifest.tests.ps1` - Module manifest validation +- `Meta.tests.ps1` - Code quality and PSScriptAnalyzer rules +- `Help.tests.ps1` - Documentation validation +- Use `ScriptAnalyzerSettings.psd1` for custom analysis rules + +### Module Compilation +The module uses **non-monolithic** compilation (`$PSBPreference.Build.CompileModule = $false`), preserving individual Public/Private .ps1 files in the output rather than combining into a single .psm1. + +## Critical Implementation Details + +### C# Embedded Code Patterns +When modifying the C# implementation: +- Use `Add-Type` with `ReferencedAssemblies` for System.Management.Automation +- Regex patterns are compiled for performance: `PropertyWithValidation`, `ArrayIndexPattern` +- Support multiple object types: Hashtables, PSObjects, .NET objects, Arrays, IList, IEnumerable + +### Array Processing +The `WildcardArrayWrapper` class enables wildcard validation by: +1. Wrapping array objects during `[*]` processing +2. Validating properties exist on ALL elements +3. Returning first element's value for continued navigation + +### Property Resolution Order +1. Check for array indexing pattern `property[index]` +2. Check for validation suffixes `property!` or `property?` +3. Handle wildcard array wrapper context +4. Fall back to regular property navigation + +## Common Patterns + +### Adding New Validation Operators +1. Update regex patterns in C# code +2. Add case handling in `ProcessPropertyWithValidation` +3. Add validation logic in `ValidatePropertyValue` +4. Update documentation and examples + +### Testing Complex Object Structures +Use the fluent syntax patterns from README.md: +```powershell +# Deep nesting with validation +Test-Exist -In $data -With "users[0].profile.settings.theme!" + +# Wildcard array validation +Test-Exist -In $data -With "users[*].email!" + +# Mixed indexing and wildcards +Test-Exist -In $data -With "orders[1].items[*].price" +``` + +### Error Message Conventions +- Include property path context: `"Property 'user.name' does not exist"` +- For arrays: `"Array index [10] is out of bounds for 'users' (length: 5)"` +- For wildcards: `"Property 'email' in element [2] is empty"` + +## Cross-Platform Considerations +The module targets PowerShell 5.1+ and supports Windows/Linux/macOS. The CI pipeline tests on all three platforms using GitHub Actions with the psmodulecache action for dependency management. diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d318c20..25c15f8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "windows": { "options": { "shell": { - "executable": "powershell.exe", + "executable": "pwsh.exe", "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command" ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 386a34a..bd78831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,32 @@ 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/). +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.1] Released +## [1.0.2] - 2025-09-24 + +### Added + +- Added test cases + +### Fixed + +- Fixed a typo in Assert-Exists where the With alias was called Width +- Fixed an issue with multi level array validation tests[*].users[1] was failing to properly validate before + +### Changed + +- Updated README +- Updated CHANGELOG + +## [1.0.1] - 2025-09-23 + +### Fixed - Fixing issue with powershell 5.1 compiling the c# code. -## [1.0.0] Released +## [1.0.0] - 2025-09-23 + +### Added - Initial release diff --git a/PSFluentObjectValidation/PSFluentObjectValidation.psd1 b/PSFluentObjectValidation/PSFluentObjectValidation.psd1 index 9cb9232..0b1541c 100644 --- a/PSFluentObjectValidation/PSFluentObjectValidation.psd1 +++ b/PSFluentObjectValidation/PSFluentObjectValidation.psd1 @@ -1,22 +1,22 @@ @{ RootModule = 'PSFluentObjectValidation.psm1' - ModuleVersion = '1.0.1' + ModuleVersion = '1.0.2' GUID = '90ac3c83-3bd9-4da5-8705-7b82b21963c8' Author = 'Joshua Wilson' CompanyName = 'PwshDevs' Copyright = '(c) 2025 PwshDevs. All rights reserved.' Description = 'Contains a helper class and functions to validate objects.' PowerShellVersion = '5.1' - FunctionsToExport = @('Test-Exists', 'Assert-Exists') + FunctionsToExport = @('Test-Exist', 'Assert-Exist') CmdletsToExport = @() VariablesToExport = '*' AliasesToExport = @('exists', 'asserts', 'tests') PrivateData = @{ PSData = @{ - Tags = @('Validation', 'Object', 'Fluent', 'Helper', 'Assert', 'Test', 'Exists') + Tags = @('PSEdition_Desktop', 'PSEdition_Core', 'Windows', 'Linux', 'MacOS', 'Validation', 'Object', 'Fluent', 'Helper', 'Assert', 'Test', 'Exists') LicenseUri = 'https://github.com/pwshdevs/PSFluentObjectValidation/blob/main/LICENSE' ProjectUri = 'https://github.com/pwshdevs/PSFluentObjectValidation' - ReleaseNotes = '' + ReleaseNotes = 'https://github.com/pwshdevs/PSFluentObjectValidation/blob/main/CHANGELOG.md' } } } diff --git a/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 b/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 index 39bfc7e..d42a561 100644 --- a/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 +++ b/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 @@ -28,7 +28,7 @@ public static class PSFluentObjectValidation if (inputObject == null) throw new ArgumentException("InputObject cannot be null"); - if (string.IsNullOrEmpty(key)) + if (String.IsNullOrEmpty(key)) throw new ArgumentException("Key cannot be null or empty"); string[] keyParts = key.Split('.'); @@ -42,6 +42,13 @@ public static class PSFluentObjectValidation private static object ProcessKeyPart(object currentObject, string part) { + // Handle wildcard array wrapper specially + if (currentObject is WildcardArrayWrapper) + { + WildcardArrayWrapper wrapper = (WildcardArrayWrapper)currentObject; + return ProcessWildcardPropertyAccess(wrapper.ArrayObject, part); + } + // Check for array indexing: property[index] or property[*] Match arrayMatch = ArrayIndexPattern.Match(part); if (arrayMatch.Success) @@ -225,6 +232,112 @@ public static class PSFluentObjectValidation private static object ProcessWildcardPropertyAccess(object arrayObject, string propertyName) { + // First check if this is an array indexing pattern: property[index] or property[*] + Match arrayMatch = ArrayIndexPattern.Match(propertyName); + if (arrayMatch.Success) + { + string basePropertyName = arrayMatch.Groups[1].Value; + string indexStr = arrayMatch.Groups[2].Value; + + // Handle array indexing after wildcard: items[0], tags[*], etc. + if (arrayObject is Array) + { + Array array = (Array)arrayObject; + for (int i = 0; i < array.Length; i++) + { + object element = array.GetValue(i); + if (element == null) + throw new InvalidOperationException(String.Format("Array element [{0}] is null", i)); + + if (!HasProperty(element, basePropertyName)) + throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName)); + + object propertyValue = GetProperty(element, basePropertyName); + if (propertyValue == null) + throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i)); + if (!IsArrayLike(propertyValue)) + throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i)); + } + + // All elements are valid, now handle the indexing + object firstElement = array.GetValue(0); + object firstPropertyValue = GetProperty(firstElement, basePropertyName); + + if (indexStr == "*") + { + return new WildcardArrayWrapper(firstPropertyValue); + } + else + { + int index = int.Parse(indexStr); + int count = GetCount(firstPropertyValue); + if (index < 0 || index >= count) + throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count)); + + if (firstPropertyValue is Array) + { + Array firstArray = (Array)firstPropertyValue; + return firstArray.GetValue(index); + } + if (firstPropertyValue is IList) + { + IList firstList = (IList)firstPropertyValue; + return firstList[index]; + } + } + } + + if (arrayObject is IList) + { + IList list = (IList)arrayObject; + for (int i = 0; i < list.Count; i++) + { + object element = list[i]; + if (element == null) + throw new InvalidOperationException(String.Format("Array element [{0}] is null", i)); + if (!HasProperty(element, basePropertyName)) + throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName)); + + object propertyValue = GetProperty(element, basePropertyName); + if (propertyValue == null) + throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i)); + if (!IsArrayLike(propertyValue)) + throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i)); + } + + // All elements are valid, now handle the indexing + object firstElement = list[0]; + object firstPropertyValue = GetProperty(firstElement, basePropertyName); + + if (indexStr == "*") + { + return new WildcardArrayWrapper(firstPropertyValue); + } + else + { + int index = int.Parse(indexStr); + int count = GetCount(firstPropertyValue); + if (index < 0 || index >= count) + throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count)); + + if (firstPropertyValue is Array) + { + Array firstArray = (Array)firstPropertyValue; + return firstArray.GetValue(index); + } + if (firstPropertyValue is IList) + { + IList firstList = (IList)firstPropertyValue; + return firstList[index]; + } + } + } + + throw new InvalidOperationException(String.Format("Cannot process wildcard array indexing on type {0}", arrayObject.GetType().Name)); + } + + + // Parse validation suffix if present Match validationMatch = PropertyWithValidation.Match(propertyName); string actualPropertyName = propertyName; diff --git a/PSFluentObjectValidation/Public/Assert-Exist.ps1 b/PSFluentObjectValidation/Public/Assert-Exist.ps1 index b9abc49..27a0c40 100644 --- a/PSFluentObjectValidation/Public/Assert-Exist.ps1 +++ b/PSFluentObjectValidation/Public/Assert-Exist.ps1 @@ -1,3 +1,39 @@ +<# +.SYNOPSIS +Asserts the existence and validity of a property within an object. + +.DESCRIPTION +The `Assert-Exist` function validates the existence of a property within an object and ensures it meets the specified validation criteria. If the validation fails, it throws a detailed exception, making it suitable for scenarios where strict validation is required. + +.PARAMETER InputObject +The object to validate. This can be a hashtable, PSObject, .NET object, or any other object type. + +.PARAMETER Key +The property path to validate. Supports fluent syntax with validation operators: +- `property.nested` - Basic navigation +- `property!` - Non-empty validation (rejects null/empty/whitespace) +- `property?` - Existence validation (allows null values) +- `array[0]` - Array indexing +- `array[*]` - Wildcard validation (all elements must pass) + +.EXAMPLE +# Validate that the `user.name` property exists and is non-empty +Assert-Exist -InputObject $data -Key "user.name!" + +.EXAMPLE +# Validate that all users in the array have a non-empty email +Assert-Exist -InputObject $data -Key "users[*].email!" + +.EXAMPLE +# Validate that the `settings.theme` property exists +Assert-Exist -InputObject $data -Key "settings.theme" + +.NOTES +Throws an exception if the validation fails. Use `Test-Exist` for a non-throwing alternative. + +.LINK +https://www.pwshdevs.com/ +#> function Assert-Exist { param( [Parameter(Mandatory=$true)] diff --git a/PSFluentObjectValidation/Public/Test-Exist.ps1 b/PSFluentObjectValidation/Public/Test-Exist.ps1 index 96540e4..3f3832f 100644 --- a/PSFluentObjectValidation/Public/Test-Exist.ps1 +++ b/PSFluentObjectValidation/Public/Test-Exist.ps1 @@ -1,3 +1,39 @@ +<# +.SYNOPSIS +Tests the existence and validity of a property within an object. + +.DESCRIPTION +The `Test-Exist` function validates the existence of a property within an object and ensures it meets the specified validation criteria. Unlike `Assert-Exist`, this function does not throw exceptions; instead, it returns a boolean value indicating whether the validation passed. + +.PARAMETER InputObject +The object to validate. This can be a hashtable, PSObject, .NET object, or any other object type. + +.PARAMETER Key +The property path to validate. Supports fluent syntax with validation operators: +- `property.nested` - Basic navigation +- `property!` - Non-empty validation (rejects null/empty/whitespace) +- `property?` - Existence validation (allows null values) +- `array[0]` - Array indexing +- `array[*]` - Wildcard validation (all elements must pass) + +.EXAMPLE +# Test if the `user.name` property exists and is non-empty +Test-Exist -InputObject $data -Key "user.name!" + +.EXAMPLE +# Test if all users in the array have a non-empty email +Test-Exist -InputObject $data -Key "users[*].email!" + +.EXAMPLE +# Test if the `settings.theme` property exists +Test-Exist -InputObject $data -Key "settings.theme" + +.NOTES +Returns `$true` if the validation passes, `$false` otherwise. Use `Assert-Exist` for a throwing alternative. + +.LINK +https://www.pwshdevs.com/ +#> Function Test-Exist { param( [Parameter(Mandatory=$true)] diff --git a/README.md b/README.md index fcb5f50..3c108f8 100644 --- a/README.md +++ b/README.md @@ -10,25 +10,23 @@ ## General Overview -The PSFluentObjectValidation represents a comprehensive evolution from simple property validation to a high-performance, feature-rich validation engine with advanced array indexing capabilities. - ## Installation ```powershell Install-PSResource -Name PSFluentObjectValidation -# or +``` + +or + +```powershell Install-Module -Name PSFluentObjectValidation +``` -# Verify installation (long style) -Test-Exists -in @{ test = "value" } -with "test!" -# (short hand) -exists -with "test!" -in @{ test = "value" } -# or alternative -tests -with "test!" -in @{ test = "value" } -# or if you'd rather have thrown errors -Assert-Exists -in @{ test = "value" } -with "test!" -# or you can use a short hand version -asserts -with "test!" -in @{ test = "value" } +Once installed, you can run a couple of tests to verify it works as expected: + +```powershell +# Verify installation +Test-Exist -In @{ test = "value" } -With "test!" # Should return: True ``` @@ -37,18 +35,23 @@ asserts -with "test!" -in @{ test = "value" } ### Basic Property Access ```powershell -exists -with "user.name" -in $data # Simple property -exists -with "user.profile.age" -in $data # Nested objects -exists -with "config.db.host" -in $data # Deep nesting +Test-Exist -With "user.name" -In $data # Simple property +Test-Exist -With "user.profile.age" -In $data # Nested objects +Test-Exist -With "config.db.host" -In $data # Deep nesting ``` -As an example, you would normally test this like so +As an example, you would normally test this like so: + ```powershell -if(-not $data.user -or -not $data.user.name -or -not $data.user.profile -or -not $data.user.profile.age) { +if(-not $data.user -or -not $data.user.name -or [string]::IsNullOrWhitespace($data.user.name) -or -not $data.user.profile -or -not $data.user.profile.age) { # do something when any of these fields do not exist } -# vs. PSFluentObjectValidation -if(-not (exists -with "user.name" -in $data) -or -not (exists -with "user.profile.age" -in $data)) { +``` + +vs. PSFluentObjectValidation + +```powershell +if(-not (Test-Exist -With "user.name!" -In $data) -or -not (Test-Exist -With "user.profile.age" -In $data)) { # do something when any of these fields do not exist } ``` @@ -57,33 +60,33 @@ if(-not (exists -with "user.name" -in $data) -or -not (exists -with "user.profil ### Validation Suffixes ```powershell -exists -with "user.email!" -in $data # Non-empty validation -exists -with "user.profile?" -in $data # Object existence -exists -with "settings.theme!" -in $data # Non-empty string +Test-Exist -With "user.email!" -In $data # Non-empty validation +Test-Exist -With "user.profile?" -In $data # Object existence +Test-Exist -With "settings.theme!" -In $data # Non-empty string ``` ### Array Indexing ```powershell -exists -with "users[0].name" -in $data # First element -exists -with "users[1].email" -in $data # Second element -exists -with "products[2].title" -in $data # Third element +Test-Exist -With "users[0].name" -In $data # First element +Test-Exist -With "users[1].email" -In $data # Second element +Test-Exist -With "products[2].title" -In $data # Third element ``` ### Wildcard Array Validation ```powershell -exists -with "users[*].name" -in $data # All users have names -exists -with "users[*].email!" -in $data # All users have non-empty emails -exists -with "products[*].active" -in $data # All products have active property +Test-Exist -With "users[*].name" -In $data # All users have names +Test-Exist -With "users[*].email!" -In $data # All users have non-empty emails +Test-Exist -With "products[*].active" -In $data # All products have active property ``` ### Advanced Combinations ```powershell -exists -with "users[0].profile.settings.theme!" -in $data # Deep + validation -exists -with "products[*].category.name!" -in $data # Wildcard + deep + validation -exists -with "orders[1].items[*].price" -in $data # Nested array access +Test-Exist -With "users[0].profile.settings.theme!" -In $data # Deep + validation +Test-Exist -With "products[*].category.name!" -In $data # Wildcard + deep + validation +Test-Exist -With "orders[1].items[*].price" -In $data # Nested array access ``` ## Usage Examples @@ -105,13 +108,13 @@ $apiResponse = @{ } # Validate all users have required fields -exists -in $apiResponse -with "users[*].id" # All users have IDs -exists -in $apiResponse -with "users[*].name!" # All users have non-empty names -exists -in $apiResponse -with "users[*].email!" # All users have non-empty emails +Test-Exist -In $apiResponse -With "users[*].id" # All users have IDs +Test-Exist -In $apiResponse -With "users[*].name!" # All users have non-empty names +Test-Exist -In $apiResponse -With "users[*].email!" # All users have non-empty emails # Validate specific user data -exists -in $apiResponse -with "users[0].active" # First user has active status -exists -in $apiResponse -with "metadata.total!" # Metadata has non-empty total +Test-Exist -In $apiResponse -With "users[0].active" # First user has active status +Test-Exist -In $apiResponse -With "metadata.total!" # Metadata has non-empty total ``` #### Configuration Validation @@ -133,10 +136,10 @@ $config = @{ } # Validate critical configuration -exists -in $config -with "database.host!" # Non-empty host -exists -in $config -with "database.credentials.password!" # Non-empty password -exists -in $config -with "servers[*].name!" # All servers have names -exists -in $config -with "servers[*].ip!" # All servers have IPs +Test-Exist -In $config -With "database.host!" # Non-empty host +Test-Exist -In $config -With "database.credentials.password!" # Non-empty password +Test-Exist -In $config -With "servers[*].name!" # All servers have names +Test-Exist -In $config -With "servers[*].ip!" # All servers have IPs ``` #### E-commerce Data Validation @@ -155,11 +158,11 @@ $order = @{ } # Comprehensive order validation -exists -in $order -with "id!" # Order has ID -exists -in $order -with "customer.email!" # Customer has email -exists -in $order -with "items[*].sku!" # All items have SKUs -exists -in $order -with "items[*].price" # All items have prices -exists -in $order -with "items[0].quantity" # First item has quantity +Test-Exist -In $order -With "id!" # Order has ID +Test-Exist -In $order -With "customer.email!" # Customer has email +Test-Exist -In $order -With "items[*].sku!" # All items have SKUs +Test-Exist -In $order -With "items[*].price" # All items have prices +Test-Exist -In $order -With "items[0].quantity" # First item has quantity ``` ## Error Handling & Edge Cases @@ -170,108 +173,43 @@ exists -in $order -with "items[0].quantity" # First item has quantity ```powershell # Array bounds checking -tests -in $data -with "users[10].name" # Returns false for out-of-bounds -asserts -in $data -with "users[10].name" # Throws: "Array index [10] is out of bounds" +Test-Exist -In $data -With "users[10].name" # Returns false for out-of-bounds +Assert-Exist -In $data -With "users[10].name" # Throws: "Array index [10] is out of bounds" # Null safety -tests -in $data -with "user.profile.settings" # Handles null intermediate objects -asserts -in $data -with "missing.property" # Throws: "Property 'missing' does not exist" +Test-Exist -In $data -With "user.profile.settings" # Handles null intermediate objects +Assert-Exist -In $data -With "missing.property" # Throws: "Property 'missing' does not exist" # Type validation -tests -in $data -with "config.port[0]" # Throws: "Property 'port' is not an array" +Test-Exist -In $data -With "config.port[0]" # Throws: "Property 'port' is not an array" ``` #### Wildcard Validation Error Handling ```powershell # Empty array validation -Test-Exists @{ users = @() } "users[*].name" # Throws: "Array 'users' is empty" +Test-Exist -In @{ users = @() } -With "users[*].name" # Throws: "Array 'users' is empty" # Partial validation failures -Test-Exists $data "users[*].email!" # Validates ALL users have non-empty emails -Assert-Exists $data "users[*].phone!" # Throws if ANY user lacks phone +Test-Exist -In $data -With "users[*].email!" # Validates ALL users have non-empty emails +Assert-Exist -In $data -With "users[*].phone!" # Throws if ANY user lacks phone ``` ### Function Reference -#### `Test-Exists` or `tests` or `exists` - -**Purpose**: Safely test property existence and validation -**Syntax**: `Test-Exists $object $propertyPath` -**Returns**: `$true` if validation passes, `$false` otherwise -**Error Handling**: Never throws exceptions - -#### `Assert-Exists` or `asserts` - -**Purpose**: Assert property existence with detailed error reporting -**Syntax**: `Assert-Exists $object $propertyPath` -**Returns**: `void` (throws on failure) -**Error Handling**: Throws descriptive exceptions for debugging - -### Advanced Configuration - -#### Performance Tuning - -- **Warmup Iterations**: Recommended 1000+ for consistent measurements -- **Test Iterations**: 10,000+ for statistical significance -- **Memory Management**: Automatic garbage collection handling - -#### Debugging Support - -- **Verbose Error Messages**: Detailed exception information -- **Stack Trace Preservation**: Full error context maintenance -- **Development Mode**: Additional validation checks available - -## Future Roadmap - -### Potential Enhancements - -1. **Dynamic Array Slicing**: `users[1:3].name` syntax -2. **Conditional Validation**: `users[active=true].email!` filtering -3. **Performance Profiling**: Built-in benchmarking tools -4. **Visual Studio Code Extension**: IntelliSense support for validation syntax - -### Performance Targets - -- **Sub-10μs Operations**: Further C# optimization -- **Parallel Validation**: Multi-threaded wildcard processing -- **Memory Optimization**: Zero-allocation validation paths - -## Performance Results Overview - -### Performance Comparison - -Originally this module was written in pure powershell but comparing it against just standard manual testing showed it was very slow (in micro seconds, still fairly quick overall). The the module was rewritten to use a different format for testing, which improved performance but it was still relatively slow compared to just manual validation. The module was then implemented in C# and saw significate performance improvements over even manual processing. The module was updated a 4th time to handle arrays as there are times when its better to test before looping with | ForEach-Object. Below is a series of tests written against all 5 methods. - -Based on 10,000 iterations with corrected V3-compatible scenario testing: - -| Version | Average Performance | vs Manual | Technology | Array Support | -|---------|-------------------|-----------|------------|---------------| -| **Manual Validation** | 59.68 μs | ±0% | Native PowerShell | Limited | -| **V1 (PowerShell)** | 352.48 μs | +491% | Pure PowerShell | ❌ | -| **V2 (PowerShell)** | 318.71 μs | +434% | Pure PowerShell | ❌ | -| **V3 (C# Basic)** | **13.06 μs** | **-78.1%** | C# Compiled | ❌ | -| **V4 (C# Current)** | **13.86 μs** | **-76.8%** | C# + Array Logic | ✅ | - -### Performance Highlights +#### `Test-Exist` -- **V3**: **78.1% faster** than manual validation - C# performance breakthrough -- **V4**: **76.8% faster** than manual validation with revolutionary array features -- **V4 vs V3**: Only **6.1% slower** - excellent trade-off for massive feature expansion -- **Both C# versions**: **27x faster** than V1, **23x faster** than V2 +> **Purpose Safely**: test property existence and validation
+> **Syntax**: `Test-Exist -In $object -With $propertyPath`
+> **Returns**: `$true` if validation passes, `$false` otherwise
+> **Error Handling**: Never throws exceptions
-## Features & Capabilities Matrix +#### `Assert-Exist` -| Feature | V1 | V2 | V3 | V4 | Description | -|---------|----|----|----|----|-------------| -| **Basic Properties** | ✅ | ✅ | ✅ | ✅ | `user.name`, `config.host` | -| **Validation Suffixes** | ✅ | ✅ | ✅ | ✅ | `property!` (non-empty), `property?` (exists) | -| **Nested Navigation** | ✅ | ✅ | ✅ | ✅ | `user.profile.settings.theme` | -| **Array Indexing** | ❌ | ❌ | ❌ | ✅ | `users[0].name`, `products[1].title` | -| **Wildcard Arrays** | ❌ | ❌ | ❌ | ✅ | `users[*].name` (all elements) | -| **Array + Validation** | ❌ | ❌ | ❌ | ✅ | `users[*].email!` (all non-empty) | -| **Deep Array Access** | ❌ | ❌ | ❌ | ✅ | `products[0].category.name` | -| **Error Handling** | Basic | Basic | Enhanced | Advanced | Bounds checking, null safety | +> **Purpose**: Assert property existence with detailed error reporting
+> **Syntax**: `Assert-Exist -In $object -With $propertyPath`
+> **Returns**: `void` (throws on failure)
+> **Error Handling**: Throws descriptive exceptions for debugging
[github-actions-badge]: https://img.shields.io/github/actions/workflow/status/pwshdevs/PSFluentObjectValidation/CI.yaml?label=build&style=for-the-badge [github-actions-badge-publish]: https://img.shields.io/github/actions/workflow/status/pwshdevs/PSFluentObjectValidation/publish.yaml?label=publish&style=for-the-badge diff --git a/docs/en-US/Assert-Exist.md b/docs/en-US/Assert-Exist.md new file mode 100644 index 0000000..edda37b --- /dev/null +++ b/docs/en-US/Assert-Exist.md @@ -0,0 +1,111 @@ +--- +external help file: PSFluentObjectValidation-help.xml +Module Name: PSFluentObjectValidation +online version: https://www.pwshdevs.com/ +schema: 2.0.0 +--- + +# Assert-Exist + +## SYNOPSIS +Asserts the existence and validity of a property within an object. + +## SYNTAX + +``` +Assert-Exist [-InputObject] [-Key] [-ProgressAction ] [] +``` + +## DESCRIPTION +The \`Assert-Exist\` function validates the existence of a property within an object and ensures it meets the specified validation criteria. +If the validation fails, it throws a detailed exception, making it suitable for scenarios where strict validation is required. + +## EXAMPLES + +### EXAMPLE 1 +``` +# Validate that the `user.name` property exists and is non-empty +Assert-Exist -InputObject $data -Key "user.name!" +``` + +### EXAMPLE 2 +``` +# Validate that all users in the array have a non-empty email +Assert-Exist -InputObject $data -Key "users[*].email!" +``` + +### EXAMPLE 3 +``` +# Validate that the `settings.theme` property exists +Assert-Exist -InputObject $data -Key "settings.theme" +``` + +## PARAMETERS + +### -InputObject +The object to validate. +This can be a hashtable, PSObject, .NET object, or any other object type. + +```yaml +Type: Object +Parameter Sets: (All) +Aliases: In + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Key +The property path to validate. +Supports fluent syntax with validation operators: +- \`property.nested\` - Basic navigation +- \`property!\` - Non-empty validation (rejects null/empty/whitespace) +- \`property?\` - Existence validation (allows null values) +- \`array\[0\]\` - Array indexing +- \`array\[*\]\` - Wildcard validation (all elements must pass) + +```yaml +Type: String +Parameter Sets: (All) +Aliases: With, Test + +Required: True +Position: 2 +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 +Throws an exception if the validation fails. +Use \`Test-Exist\` for a non-throwing alternative. + +## RELATED LINKS + +[https://www.pwshdevs.com/](https://www.pwshdevs.com/) + diff --git a/docs/en-US/Test-Exist.md b/docs/en-US/Test-Exist.md new file mode 100644 index 0000000..a0ab182 --- /dev/null +++ b/docs/en-US/Test-Exist.md @@ -0,0 +1,111 @@ +--- +external help file: PSFluentObjectValidation-help.xml +Module Name: PSFluentObjectValidation +online version: https://www.pwshdevs.com/ +schema: 2.0.0 +--- + +# Test-Exist + +## SYNOPSIS +Tests the existence and validity of a property within an object. + +## SYNTAX + +``` +Test-Exist [-InputObject] [-Key] [-ProgressAction ] [] +``` + +## DESCRIPTION +The \`Test-Exist\` function validates the existence of a property within an object and ensures it meets the specified validation criteria. +Unlike \`Assert-Exist\`, this function does not throw exceptions; instead, it returns a boolean value indicating whether the validation passed. + +## EXAMPLES + +### EXAMPLE 1 +``` +# Test if the `user.name` property exists and is non-empty +Test-Exist -InputObject $data -Key "user.name!" +``` + +### EXAMPLE 2 +``` +# Test if all users in the array have a non-empty email +Test-Exist -InputObject $data -Key "users[*].email!" +``` + +### EXAMPLE 3 +``` +# Test if the `settings.theme` property exists +Test-Exist -InputObject $data -Key "settings.theme" +``` + +## PARAMETERS + +### -InputObject +The object to validate. +This can be a hashtable, PSObject, .NET object, or any other object type. + +```yaml +Type: Object +Parameter Sets: (All) +Aliases: In + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Key +The property path to validate. +Supports fluent syntax with validation operators: +- \`property.nested\` - Basic navigation +- \`property!\` - Non-empty validation (rejects null/empty/whitespace) +- \`property?\` - Existence validation (allows null values) +- \`array\[0\]\` - Array indexing +- \`array\[*\]\` - Wildcard validation (all elements must pass) + +```yaml +Type: String +Parameter Sets: (All) +Aliases: With, Test + +Required: True +Position: 2 +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 +Returns \`$true\` if the validation passes, \`$false\` otherwise. +Use \`Assert-Exist\` for a throwing alternative. + +## RELATED LINKS + +[https://www.pwshdevs.com/](https://www.pwshdevs.com/) + diff --git a/tests/PSFluentObjectValidation.tests.ps1 b/tests/PSFluentObjectValidation.tests.ps1 new file mode 100644 index 0000000..fb0a1a7 --- /dev/null +++ b/tests/PSFluentObjectValidation.tests.ps1 @@ -0,0 +1,505 @@ +BeforeAll { + # Remove any existing module from session to avoid conflicts + if (Get-Module PSFluentObjectValidation) { + Remove-Module PSFluentObjectValidation -Force + } + + # Import the module from the build output directory (if it exists) or development directory + $OutputModulePath = "$PSScriptRoot\..\Output\PSFluentObjectValidation\1.0.2\PSFluentObjectValidation.psd1" + $DevModulePath = "$PSScriptRoot\..\PSFluentObjectValidation\PSFluentObjectValidation.psd1" + + if (Test-Path $OutputModulePath) { + Import-Module $OutputModulePath -Force + } elseif (Test-Path $DevModulePath) { + Import-Module $DevModulePath -Force + } else { + throw "Could not find PSFluentObjectValidation module at expected paths" + } +} + +Describe 'PSFluentObjectValidation Module Tests' { + + BeforeAll { + # Test data structures for comprehensive validation + $script:TestData = @{ + # Simple properties + name = "John Doe" + age = 30 + active = $true + + # Null and empty values + nullValue = $null + emptyString = "" + whitespaceString = " " + emptyArray = @() + + # Nested objects + user = @{ + profile = @{ + email = "john@example.com" + settings = @{ + theme = "dark" + notifications = $true + } + } + preferences = @{ + language = "en" + timezone = $null + } + } + + # Arrays with objects + users = @( + @{ id = 1; name = "Alice"; email = "alice@test.com"; active = $true } + @{ id = 2; name = "Bob"; email = "bob@test.com"; active = $false } + @{ id = 3; name = "Charlie"; email = ""; active = $true } + @{ id = 4; name = ""; email = "diana@test.com"; active = $true } + ) + + # Mixed array types + products = @( + @{ + id = 1 + title = "Laptop" + category = @{ name = "Electronics"; active = $true } + tags = @("computer", "portable") + price = 999.99 + } + @{ + id = 2 + title = "Mouse" + category = @{ name = "Accessories"; active = $true } + tags = @("peripheral") + price = 29.99 + } + ) + + # Complex nested structure + orders = @( + @{ + id = "ORD-001" + customer = @{ + name = "John Smith" + email = "john.smith@example.com" + } + items = @( + @{ sku = "LAPTOP-001"; quantity = 1; price = 999.99 } + @{ sku = "MOUSE-001"; quantity = 2; price = 29.99 } + ) + metadata = @{ + source = "web" + campaign = $null + } + } + @{ + id = "ORD-002" + customer = @{ + name = "Jane Doe" + email = "jane@example.com" + } + items = @( + @{ sku = "KEYBOARD-001"; quantity = 1; price = 79.99 } + ) + metadata = @{ + source = "mobile" + campaign = "SUMMER2025" + } + } + ) + + # Empty nested structures + emptyStructure = @{ + emptyArray = @() + emptyObject = @{} + nullArray = $null + } + } + + # PSObject test data + $script:PSObjectData = New-Object PSObject -Property @{ + name = "PSObject Test" + nested = New-Object PSObject -Property @{ + value = 42 + array = @(1, 2, 3) + } + } + + # .NET object test data + $script:NetObjectData = [PSCustomObject]@{ + Property1 = "Value1" + Property2 = [PSCustomObject]@{ + SubProperty = "SubValue" + } + } + } + + Context 'Test-Exist Function Tests' { + + Describe 'Basic Property Access' { + + It 'Should return true for existing simple properties' { + Test-Exist -In $TestData -With "name" | Should -Be $true + Test-Exist -In $TestData -With "age" | Should -Be $true + Test-Exist -In $TestData -With "active" | Should -Be $true + } + + It 'Should return false for non-existing properties' { + Test-Exist -In $TestData -With "nonexistent" | Should -Be $false + Test-Exist -In $TestData -With "missing.property" | Should -Be $false + } + + It 'Should handle null values gracefully' { + Test-Exist -In $TestData -With "nullValue" | Should -Be $true + } + + It 'Should work with nested properties' { + Test-Exist -In $TestData -With "user.profile.email" | Should -Be $true + Test-Exist -In $TestData -With "user.profile.settings.theme" | Should -Be $true + Test-Exist -In $TestData -With "user.preferences.language" | Should -Be $true + } + + It 'Should return false for missing nested properties' { + Test-Exist -In $TestData -With "user.profile.missing" | Should -Be $false + Test-Exist -In $TestData -With "user.missing.property" | Should -Be $false + Test-Exist -In $TestData -With "missing.user.property" | Should -Be $false + } + } + + Describe 'Validation Operators' { + + Context 'Non-empty validation (!)' { + + It 'Should return true for non-empty values' { + Test-Exist -In $TestData -With "name!" | Should -Be $true + Test-Exist -In $TestData -With "user.profile.email!" | Should -Be $true + Test-Exist -In $TestData -With "user.profile.settings.theme!" | Should -Be $true + } + + It 'Should return false for null values' { + Test-Exist -In $TestData -With "nullValue!" | Should -Be $false + Test-Exist -In $TestData -With "user.preferences.timezone!" | Should -Be $false + } + + It 'Should return false for empty strings' { + Test-Exist -In $TestData -With "emptyString!" | Should -Be $false + Test-Exist -In $TestData -With "whitespaceString!" | Should -Be $false + } + + It 'Should return false for empty arrays' { + Test-Exist -In $TestData -With "emptyArray!" | Should -Be $false + Test-Exist -In $TestData -With "emptyStructure.emptyArray!" | Should -Be $false + } + + It 'Should return false for non-existing properties' { + Test-Exist -In $TestData -With "nonexistent!" | Should -Be $false + } + } + + Context 'Existence validation (?)' { + + It 'Should return true for existing properties even if null' { + Test-Exist -In $TestData -With "nullValue?" | Should -Be $true + Test-Exist -In $TestData -With "user.preferences.timezone?" | Should -Be $true + } + + It 'Should return true for non-empty values' { + Test-Exist -In $TestData -With "name?" | Should -Be $true + Test-Exist -In $TestData -With "user.profile.email?" | Should -Be $true + } + + It 'Should return false for empty arrays' { + Test-Exist -In $TestData -With "emptyArray?" | Should -Be $false + Test-Exist -In $TestData -With "emptyStructure.emptyArray?" | Should -Be $false + } + + It 'Should return false for non-existing properties' { + Test-Exist -In $TestData -With "nonexistent?" | Should -Be $false + } + } + } + + Describe 'Array Indexing' { + + It 'Should access array elements by index' { + Test-Exist -In $TestData -With "users[0]" | Should -Be $true + Test-Exist -In $TestData -With "users[1]" | Should -Be $true + Test-Exist -In $TestData -With "users[2]" | Should -Be $true + Test-Exist -In $TestData -With "users[3]" | Should -Be $true + } + + It 'Should access properties of array elements' { + Test-Exist -In $TestData -With "users[0].name" | Should -Be $true + Test-Exist -In $TestData -With "users[1].email" | Should -Be $true + Test-Exist -In $TestData -With "users[0].id" | Should -Be $true + } + + It 'Should return false for out-of-bounds indices' { + Test-Exist -In $TestData -With "users[10]" | Should -Be $false + Test-Exist -In $TestData -With "users[100].name" | Should -Be $false + } + + It 'Should handle negative indices gracefully' { + Test-Exist -In $TestData -With "users[-1]" | Should -Be $false + } + + It 'Should work with nested array access' { + Test-Exist -In $TestData -With "orders[0].items[0]" | Should -Be $true + Test-Exist -In $TestData -With "orders[0].items[0].sku" | Should -Be $true + Test-Exist -In $TestData -With "orders[1].items[0].price" | Should -Be $true + } + } + + Describe 'Wildcard Array Validation' { + + It 'Should validate properties exist on all array elements' { + Test-Exist -In $TestData -With "users[*].id" | Should -Be $true + Test-Exist -In $TestData -With "users[*].name" | Should -Be $true + Test-Exist -In $TestData -With "users[*].active" | Should -Be $true + } + + It 'Should return false if any element is missing the property' { + Test-Exist -In $TestData -With "users[*].missing" | Should -Be $false + } + + It 'Should work with wildcard validation operators' { + Test-Exist -In $TestData -With "users[*].id!" | Should -Be $true + Test-Exist -In $TestData -With "users[*].email!" | Should -Be $false # One user has empty email + Test-Exist -In $TestData -With "users[*].name!" | Should -Be $false # One user has empty name + } + + It 'Should work with nested wildcard validation' { + Test-Exist -In $TestData -With "products[*].category.name" | Should -Be $true + Test-Exist -In $TestData -With "products[*].category.name!" | Should -Be $true + Test-Exist -In $TestData -With "orders[*].customer.email!" | Should -Be $true + } + + It 'Should handle empty arrays' { + Test-Exist -In $TestData -With "emptyArray[*].property" | Should -Be $false + Test-Exist -In $TestData -With "emptyStructure.emptyArray[*].property" | Should -Be $false + } + } + + Describe 'Complex Combinations' { + + It 'Should handle deep nesting with validation' { + Test-Exist -In $TestData -With "user.profile.settings.theme!" | Should -Be $true + Test-Exist -In $TestData -With "orders[0].customer.email!" | Should -Be $true + Test-Exist -In $TestData -With "products[0].category.active?" | Should -Be $true + } + + It 'Should handle mixed indexing and wildcards' { + Test-Exist -In $TestData -With "orders[0].items[*].sku" | Should -Be $true + Test-Exist -In $TestData -With "orders[1].items[*].price!" | Should -Be $true + # Fixed: Complex wildcard+indexing patterns now work + Test-Exist -In $TestData -With "orders[*].items[0].quantity" | Should -Be $true + } + + It 'Should validate complex nested structures' { + Test-Exist -In $TestData -With "orders[*].customer.name!" | Should -Be $true + Test-Exist -In $TestData -With "orders[*].metadata.source!" | Should -Be $true + Test-Exist -In $TestData -With "orders[*].metadata.campaign!" | Should -Be $false # One order has null campaign + } + } + + Describe 'Different Object Types' { + + It 'Should work with PSObjects' { + Test-Exist -In $PSObjectData -With "name" | Should -Be $true + Test-Exist -In $PSObjectData -With "nested.value" | Should -Be $true + Test-Exist -In $PSObjectData -With "nested.array[0]" | Should -Be $true + } + + It 'Should work with .NET objects' { + Test-Exist -In $NetObjectData -With "Property1" | Should -Be $true + Test-Exist -In $NetObjectData -With "Property2.SubProperty" | Should -Be $true + } + } + + Describe 'Edge Cases' { + + It 'Should handle null input objects gracefully' { + # PowerShell parameter binding prevents null InputObject, so we test the C# class directly + [PSFluentObjectValidation]::TestExists($null, "property") | Should -Be $false + } + + It 'Should handle empty property paths gracefully' { + # PowerShell parameter binding prevents empty Key, so we test the C# class directly + [PSFluentObjectValidation]::TestExists($TestData, "") | Should -Be $false + } + + It 'Should handle invalid array indices gracefully' { + Test-Exist -In $TestData -With "users[abc]" | Should -Be $false + Test-Exist -In $TestData -With "users[]" | Should -Be $false + } + + It 'Should handle properties that are not arrays when using array syntax' { + Test-Exist -In $TestData -With "name[0]" | Should -Be $false + Test-Exist -In $TestData -With "age[*]" | Should -Be $false + } + } + } + + Context 'Assert-Exist Function Tests' { + + Describe 'Basic Property Access' { + + It 'Should not throw for existing properties' { + { Assert-Exist -In $TestData -With "name" } | Should -Not -Throw + { Assert-Exist -In $TestData -With "user.profile.email" } | Should -Not -Throw + } + + It 'Should throw for non-existing properties' { + { Assert-Exist -In $TestData -With "nonexistent" } | Should -Throw + { Assert-Exist -In $TestData -With "user.profile.missing" } | Should -Throw + } + + It 'Should throw with descriptive error messages for missing properties' { + { Assert-Exist -In $TestData -With "nonexistent" } | Should -Throw -ExpectedMessage "*Property 'nonexistent' does not exist*" + { Assert-Exist -In $TestData -With "user.missing" } | Should -Throw -ExpectedMessage "*Property 'missing' does not exist*" + } + } + + Describe 'Validation Operators' { + + Context 'Non-empty validation (!)' { + + It 'Should not throw for non-empty values' { + { Assert-Exist -In $TestData -With "name!" } | Should -Not -Throw + { Assert-Exist -In $TestData -With "user.profile.email!" } | Should -Not -Throw + } + + It 'Should throw for null values' { + { Assert-Exist -In $TestData -With "nullValue!" } | Should -Throw -ExpectedMessage "*is null*" + { Assert-Exist -In $TestData -With "user.preferences.timezone!" } | Should -Throw -ExpectedMessage "*is null*" + } + + It 'Should throw for empty strings' { + { Assert-Exist -In $TestData -With "emptyString!" } | Should -Throw -ExpectedMessage "*is empty*" + { Assert-Exist -In $TestData -With "whitespaceString!" } | Should -Throw -ExpectedMessage "*is empty or whitespace*" + } + + It 'Should throw for empty arrays' { + { Assert-Exist -In $TestData -With "emptyArray!" } | Should -Throw -ExpectedMessage "*is empty*" + } + } + + Context 'Existence validation (?)' { + + It 'Should not throw for existing properties even if null' { + { Assert-Exist -In $TestData -With "nullValue?" } | Should -Not -Throw + { Assert-Exist -In $TestData -With "user.preferences.timezone?" } | Should -Not -Throw + } + + It 'Should throw for empty arrays' { + { Assert-Exist -In $TestData -With "emptyArray?" } | Should -Throw -ExpectedMessage "*is empty*" + } + + It 'Should throw for non-existing properties' { + { Assert-Exist -In $TestData -With "nonexistent?" } | Should -Throw -ExpectedMessage "*does not exist*" + } + } + } + + Describe 'Array Indexing' { + + It 'Should not throw for valid array indices' { + { Assert-Exist -In $TestData -With "users[0]" } | Should -Not -Throw + { Assert-Exist -In $TestData -With "users[0].name" } | Should -Not -Throw + } + + It 'Should throw for out-of-bounds indices with descriptive messages' { + { Assert-Exist -In $TestData -With "users[10]" } | Should -Throw -ExpectedMessage "*Array index*out of bounds*" + { Assert-Exist -In $TestData -With "users[100]" } | Should -Throw -ExpectedMessage "*Array index*out of bounds*" + } + + It 'Should throw when trying to index non-array properties' { + { Assert-Exist -In $TestData -With "name[0]" } | Should -Throw -ExpectedMessage "*is not an array*" + { Assert-Exist -In $TestData -With "age[*]" } | Should -Throw -ExpectedMessage "*is not an array*" + } + } + + Describe 'Wildcard Array Validation' { + + It 'Should not throw when all elements have the property' { + { Assert-Exist -In $TestData -With "users[*].id" } | Should -Not -Throw + { Assert-Exist -In $TestData -With "products[*].title" } | Should -Not -Throw + } + + It 'Should throw when any element is missing the property' { + { Assert-Exist -In $TestData -With "users[*].missing" } | Should -Throw -ExpectedMessage "*does not have property*" + } + + It 'Should throw with element context for validation failures' { + { Assert-Exist -In $TestData -With "users[*].email!" } | Should -Throw -ExpectedMessage "*element*empty*" + { Assert-Exist -In $TestData -With "users[*].name!" } | Should -Throw -ExpectedMessage "*element*empty*" + } + + It 'Should throw for empty arrays with wildcard' { + { Assert-Exist -In $TestData -With "emptyArray[*].property" } | Should -Throw -ExpectedMessage "*empty*cannot validate*" + } + } + + Describe 'Error Message Quality' { + + It 'Should provide context in error messages for deep nesting' { + { Assert-Exist -In $TestData -With "user.profile.missing.deep" } | Should -Throw -ExpectedMessage "*Property 'missing' does not exist*" + } + + It 'Should provide array context in error messages' { + { Assert-Exist -In $TestData -With "users[0].missing" } | Should -Throw -ExpectedMessage "*Property 'missing' does not exist*" + } + + It 'Should provide validation context in error messages' { + { Assert-Exist -In $TestData -With "orders[0].metadata.campaign!" } | Should -Throw -ExpectedMessage "*is null*" + } + } + + Describe 'Input Validation' { + + It 'Should throw for null input objects' { + # PowerShell parameter binding prevents null, so test C# class directly + { [PSFluentObjectValidation]::AssertExists($null, "property") } | Should -Throw -ExpectedMessage "*InputObject cannot be null*" + } + + It 'Should throw for null or empty keys' { + # PowerShell parameter binding prevents null/empty, so test C# class directly + { [PSFluentObjectValidation]::AssertExists($TestData, $null) } | Should -Throw -ExpectedMessage "*Key cannot be null or empty*" + { [PSFluentObjectValidation]::AssertExists($TestData, "") } | Should -Throw -ExpectedMessage "*Key cannot be null or empty*" + } + } + } + + Context 'Function Aliases' { + + It 'Should support Test-Exist aliases' { + exists -In $TestData -With "name" | Should -Be $true + tests -In $TestData -With "user.profile.email" | Should -Be $true + } + + It 'Should support Assert-Exist aliases' { + { asserts -In $TestData -With "name" } | Should -Not -Throw + } + } + + Context 'Performance and Edge Cases' { + + It 'Should handle deeply nested structures efficiently' { + $deepData = @{ level1 = @{ level2 = @{ level3 = @{ level4 = @{ value = "deep" } } } } } + Test-Exist -In $deepData -With "level1.level2.level3.level4.value" | Should -Be $true + } + + It 'Should handle large arrays efficiently' { + $largeArray = 1..100 | ForEach-Object { @{ id = $_; name = "Item$_" } } + $largeData = @{ items = $largeArray } + Test-Exist -In $largeData -With "items[99].name" | Should -Be $true + Test-Exist -In $largeData -With "items[*].id" | Should -Be $true + } + + It 'Should handle mixed validation patterns' { + Test-Exist -In $TestData -With "orders[0].items[*].sku!" | Should -Be $true + Test-Exist -In $TestData -With "orders[*].customer.name!" | Should -Be $true + # Fixed: Wildcard followed by array indexing now works + Test-Exist -In $TestData -With "products[*].tags[0]" | Should -Be $true + } + } +} From b819e334fc5f3eb60ebf1713780f08f43ceb7895 Mon Sep 17 00:00:00 2001 From: kormic911 Date: Wed, 24 Sep 2025 21:45:58 -0500 Subject: [PATCH 5/5] Updating Help docs, updating psscriptanalyzer results --- .../Private/PSFluentObjectValidation.ps1 | 28 ++--- .../Public/Assert-Exist.ps1 | 12 +- .../Public/Test-Exist.ps1 | 11 +- README.md | 5 +- docs/en-US/Assert-Exist.md | 9 +- docs/en-US/Test-Exist.md | 9 +- .../about_PSFluentObjectValidation.help.md | 59 --------- tests/Help.tests.old.ps1 | 117 ++++++++++++++++++ tests/Help.tests.ps1 | 25 ++-- 9 files changed, 177 insertions(+), 98 deletions(-) delete mode 100644 docs/en-US/about_PSFluentObjectValidation.help.md create mode 100644 tests/Help.tests.old.ps1 diff --git a/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 b/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 index d42a561..742c7a6 100644 --- a/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 +++ b/PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1 @@ -48,7 +48,7 @@ public static class PSFluentObjectValidation WildcardArrayWrapper wrapper = (WildcardArrayWrapper)currentObject; return ProcessWildcardPropertyAccess(wrapper.ArrayObject, part); } - + // Check for array indexing: property[index] or property[*] Match arrayMatch = ArrayIndexPattern.Match(part); if (arrayMatch.Success) @@ -232,13 +232,13 @@ public static class PSFluentObjectValidation private static object ProcessWildcardPropertyAccess(object arrayObject, string propertyName) { - // First check if this is an array indexing pattern: property[index] or property[*] + // First check if this is an array indexing pattern: property[index] or property[*] Match arrayMatch = ArrayIndexPattern.Match(propertyName); if (arrayMatch.Success) { string basePropertyName = arrayMatch.Groups[1].Value; string indexStr = arrayMatch.Groups[2].Value; - + // Handle array indexing after wildcard: items[0], tags[*], etc. if (arrayObject is Array) { @@ -248,21 +248,21 @@ public static class PSFluentObjectValidation object element = array.GetValue(i); if (element == null) throw new InvalidOperationException(String.Format("Array element [{0}] is null", i)); - + if (!HasProperty(element, basePropertyName)) throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName)); - + object propertyValue = GetProperty(element, basePropertyName); if (propertyValue == null) throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i)); if (!IsArrayLike(propertyValue)) throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i)); } - + // All elements are valid, now handle the indexing object firstElement = array.GetValue(0); object firstPropertyValue = GetProperty(firstElement, basePropertyName); - + if (indexStr == "*") { return new WildcardArrayWrapper(firstPropertyValue); @@ -273,7 +273,7 @@ public static class PSFluentObjectValidation int count = GetCount(firstPropertyValue); if (index < 0 || index >= count) throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count)); - + if (firstPropertyValue is Array) { Array firstArray = (Array)firstPropertyValue; @@ -286,7 +286,7 @@ public static class PSFluentObjectValidation } } } - + if (arrayObject is IList) { IList list = (IList)arrayObject; @@ -297,18 +297,18 @@ public static class PSFluentObjectValidation throw new InvalidOperationException(String.Format("Array element [{0}] is null", i)); if (!HasProperty(element, basePropertyName)) throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName)); - + object propertyValue = GetProperty(element, basePropertyName); if (propertyValue == null) throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i)); if (!IsArrayLike(propertyValue)) throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i)); } - + // All elements are valid, now handle the indexing object firstElement = list[0]; object firstPropertyValue = GetProperty(firstElement, basePropertyName); - + if (indexStr == "*") { return new WildcardArrayWrapper(firstPropertyValue); @@ -319,7 +319,7 @@ public static class PSFluentObjectValidation int count = GetCount(firstPropertyValue); if (index < 0 || index >= count) throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count)); - + if (firstPropertyValue is Array) { Array firstArray = (Array)firstPropertyValue; @@ -332,7 +332,7 @@ public static class PSFluentObjectValidation } } } - + throw new InvalidOperationException(String.Format("Cannot process wildcard array indexing on type {0}", arrayObject.GetType().Name)); } diff --git a/PSFluentObjectValidation/Public/Assert-Exist.ps1 b/PSFluentObjectValidation/Public/Assert-Exist.ps1 index 27a0c40..e6a2380 100644 --- a/PSFluentObjectValidation/Public/Assert-Exist.ps1 +++ b/PSFluentObjectValidation/Public/Assert-Exist.ps1 @@ -17,17 +17,17 @@ The property path to validate. Supports fluent syntax with validation operators: - `array[*]` - Wildcard validation (all elements must pass) .EXAMPLE -# Validate that the `user.name` property exists and is non-empty Assert-Exist -InputObject $data -Key "user.name!" +Asserts that the `user.name` property exists and is non-empty .EXAMPLE -# Validate that all users in the array have a non-empty email Assert-Exist -InputObject $data -Key "users[*].email!" +Asserts that all users in the array have a non-empty email .EXAMPLE -# Validate that the `settings.theme` property exists Assert-Exist -InputObject $data -Key "settings.theme" +Asserts that the `settings.theme` property exists .NOTES Throws an exception if the validation fails. Use `Test-Exist` for a non-throwing alternative. @@ -44,7 +44,11 @@ function Assert-Exist { [string]$Key ) - [PSFluentObjectValidation]::AssertExists($InputObject, $Key) + Begin { } + + Process { + [PSFluentObjectValidation]::AssertExists($InputObject, $Key) + } } New-Alias -Name asserts -Value Assert-Exist diff --git a/PSFluentObjectValidation/Public/Test-Exist.ps1 b/PSFluentObjectValidation/Public/Test-Exist.ps1 index 3f3832f..ea80bca 100644 --- a/PSFluentObjectValidation/Public/Test-Exist.ps1 +++ b/PSFluentObjectValidation/Public/Test-Exist.ps1 @@ -17,17 +17,17 @@ The property path to validate. Supports fluent syntax with validation operators: - `array[*]` - Wildcard validation (all elements must pass) .EXAMPLE -# Test if the `user.name` property exists and is non-empty Test-Exist -InputObject $data -Key "user.name!" +Tests that the `user.name` property exists and is non-empty. .EXAMPLE -# Test if all users in the array have a non-empty email Test-Exist -InputObject $data -Key "users[*].email!" +Tests that all users in the array have a non-empty email. .EXAMPLE -# Test if the `settings.theme` property exists Test-Exist -InputObject $data -Key "settings.theme" +Tests that the `settings.theme` property exists. .NOTES Returns `$true` if the validation passes, `$false` otherwise. Use `Assert-Exist` for a throwing alternative. @@ -44,7 +44,10 @@ Function Test-Exist { [string]$Key ) - return [PSFluentObjectValidation]::TestExists($InputObject, $Key) + Begin { } + Process { + return [PSFluentObjectValidation]::TestExists($InputObject, $Key) + } } New-Alias -Name exists -Value Test-Exist diff --git a/README.md b/README.md index 3c108f8..6a224b7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **PSGallery** -[![PowerShell Gallery][psgallery-badge]][psgallery] [![PSGallery Version][psgallery-version-badge]][psgallery] +[![PowerShell Gallery][psgallery-badge]][psgallery] [![PSGallery Version][psgallery-version-badge]][psgallery] [![PSGallery Playform][psgallery-platform-badge]][psgallery] [![PSGallery Playform][ps-desktop-badge]][psgallery] ## General Overview @@ -223,3 +223,6 @@ Assert-Exist -In $data -With "users[*].phone!" # Throws if ANY user [github-closed-issues-badge]: https://img.shields.io/github/issues-closed/pwshdevs/PSFluentObjectValidation?style=for-the-badge [github-closed-issues]: https://github.com/pwshdevs/PSFluentObjectValidation/issues?q=is%3Aissue%20state%3Aclosed [github-open-issues]: https://github.com/pwshdevs/PSFluentObjectValidation/issues +[psgallery-platform-badge]: https://img.shields.io/powershellgallery/p/PSFluentObjectValidation?style=for-the-badge +[ps-desktop-badge]: https://img.shields.io/badge/powershell-5.1,_7.0+-blue?style=for-the-badge +[ps-core-badge]: https://img.shields.io/badge/powershell-5.1,_7.0+-blue?style=for-the-badge diff --git a/docs/en-US/Assert-Exist.md b/docs/en-US/Assert-Exist.md index edda37b..c162cdd 100644 --- a/docs/en-US/Assert-Exist.md +++ b/docs/en-US/Assert-Exist.md @@ -24,22 +24,25 @@ If the validation fails, it throws a detailed exception, making it suitable for ### EXAMPLE 1 ``` -# Validate that the `user.name` property exists and is non-empty Assert-Exist -InputObject $data -Key "user.name!" ``` +Asserts that the \`user.name\` property exists and is non-empty + ### EXAMPLE 2 ``` -# Validate that all users in the array have a non-empty email Assert-Exist -InputObject $data -Key "users[*].email!" ``` +Asserts that all users in the array have a non-empty email + ### EXAMPLE 3 ``` -# Validate that the `settings.theme` property exists Assert-Exist -InputObject $data -Key "settings.theme" ``` +Asserts that the \`settings.theme\` property exists + ## PARAMETERS ### -InputObject diff --git a/docs/en-US/Test-Exist.md b/docs/en-US/Test-Exist.md index a0ab182..6f3bf81 100644 --- a/docs/en-US/Test-Exist.md +++ b/docs/en-US/Test-Exist.md @@ -24,22 +24,25 @@ Unlike \`Assert-Exist\`, this function does not throw exceptions; instead, it re ### EXAMPLE 1 ``` -# Test if the `user.name` property exists and is non-empty Test-Exist -InputObject $data -Key "user.name!" ``` +Tests that the \`user.name\` property exists and is non-empty. + ### EXAMPLE 2 ``` -# Test if all users in the array have a non-empty email Test-Exist -InputObject $data -Key "users[*].email!" ``` +Tests that all users in the array have a non-empty email. + ### EXAMPLE 3 ``` -# Test if the `settings.theme` property exists Test-Exist -InputObject $data -Key "settings.theme" ``` +Tests that the \`settings.theme\` property exists. + ## PARAMETERS ### -InputObject diff --git a/docs/en-US/about_PSFluentObjectValidation.help.md b/docs/en-US/about_PSFluentObjectValidation.help.md deleted file mode 100644 index 4a896a5..0000000 --- a/docs/en-US/about_PSFluentObjectValidation.help.md +++ /dev/null @@ -1,59 +0,0 @@ -# PSFluentObjectValidation - -## about_PSFluentObjectValidation - -``` -ABOUT TOPIC NOTE: -The first header of the about topic should be the topic name. -The second header contains the lookup name used by the help system. - -IE: -# Some Help Topic Name -## SomeHelpTopicFileName - -This will be transformed into the text file -as `about_SomeHelpTopicFileName`. -Do not include file extensions. -The second header should have no spaces. -``` - -# SHORT DESCRIPTION -{{ Short Description Placeholder }} - -``` -ABOUT TOPIC NOTE: -About topics can be no longer than 80 characters wide when rendered to text. -Any topics greater than 80 characters will be automatically wrapped. -The generated about topic will be encoded UTF-8. -``` - -# LONG DESCRIPTION -{{ Long Description Placeholder }} - -## Optional Subtopics -{{ Optional Subtopic Placeholder }} - -# EXAMPLES -{{ Code or descriptive examples of how to leverage the functions described. }} - -# NOTE -{{ Note Placeholder - Additional information that a user needs to know.}} - -# TROUBLESHOOTING NOTE -{{ Troubleshooting Placeholder - Warns users of bugs}} - -{{ Explains behavior that is likely to change with fixes }} - -# SEE ALSO -{{ See also placeholder }} - -{{ You can also list related articles, blogs, and video URLs. }} - -# KEYWORDS -{{List alternate names or titles for this topic that readers might use.}} - -- {{ Keyword Placeholder }} -- {{ Keyword Placeholder }} -- {{ Keyword Placeholder }} -- {{ Keyword Placeholder }} - diff --git a/tests/Help.tests.old.ps1 b/tests/Help.tests.old.ps1 new file mode 100644 index 0000000..8a6e175 --- /dev/null +++ b/tests/Help.tests.old.ps1 @@ -0,0 +1,117 @@ +# Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) + +BeforeDiscovery { + + function global:FilterOutCommonParams { + param ($Params) + $commonParams = @( + 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', + 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', + 'WarningVariable', 'Confirm', 'Whatif' + ) + $params | Where-Object { $_.Name -notin $commonParams } | Sort-Object -Property Name -Unique + } + + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" + + # 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 + Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop + $params = @{ + Module = (Get-Module $env:BHProjectName) + CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias + } + if ($PSVersionTable.PSVersion.Major -lt 6) { + $params.CommandType[0] += 'Workflow' + } + $commands = Get-Command @params + + ## When testing help, remember that help is cached at the beginning of each session. + ## To test, restart session. +} + +Describe "Test help for <_.Name>" -ForEach $commands { + + BeforeDiscovery { + # Get command help, parameters, and links + $command = $_ + $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters + $commandParameterNames = $commandParameters.Name + $helpLinks = $commandHelp.relatedLinks.navigationLink.uri + } + + BeforeAll { + # These vars are needed in both discovery and test phases so we need to duplicate them here + $command = $_ + $commandName = $_.Name + $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters + $commandParameterNames = $commandParameters.Name + $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter + $helpParameterNames = $helpParameters.Name + } + + # If help is not found, synopsis in auto-generated help is the syntax diagram + It 'Help is not auto-generated' { + $commandHelp.Synopsis | Should -Not -BeLike '*`[``]*' + } + + # Should be a description for every function + It "Has description" { + $commandHelp.Description | Should -Not -BeNullOrEmpty + } + + # Should be at least one example + It "Has example code" { + ($commandHelp.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty + } + + # Should be at least one example description + It "Has example help" { + ($commandHelp.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty + } + + It "Help link <_> is valid" -ForEach $helpLinks { + (Invoke-WebRequest -Uri $_ -UseBasicParsing).StatusCode | Should -Be '200' + } + + Context "Parameter <_.Name>" -Foreach $commandParameters { + + BeforeAll { + $parameter = $_ + $parameterName = $parameter.Name + $parameterHelp = $commandHelp.parameters.parameter | Where-Object Name -eq $parameterName + $parameterHelpType = if ($parameterHelp.ParameterValue) { $parameterHelp.ParameterValue.Trim() } + } + + # Should be a description for every parameter + It "Has description" { + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty + } + + # Required value in Help should match IsMandatory property of parameter + It "Has correct [mandatory] value" { + $codeMandatory = $_.IsMandatory.toString() + $parameterHelp.Required | Should -Be $codeMandatory + } + + # Parameter type in help should match code + It "Has correct parameter type" { + $parameterHelpType | Should -Be $parameter.ParameterType.Name + } + } + + Context "Test <_> help parameter help for " -Foreach $helpParameterNames { + + # Shouldn't find extra parameters in help. + It "finds help parameter in code: <_>" { + $_ -in $parameterNames | Should -Be $true + } + } +} diff --git a/tests/Help.tests.ps1 b/tests/Help.tests.ps1 index 8a6e175..2bca45e 100644 --- a/tests/Help.tests.ps1 +++ b/tests/Help.tests.ps1 @@ -1,14 +1,10 @@ # Taken with love from @juneb_get_help (https://raw.githubusercontent.com/juneb/PesterTDD/master/Module.Help.Tests.ps1) BeforeDiscovery { - function global:FilterOutCommonParams { param ($Params) - $commonParams = @( - 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', - 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', - 'WarningVariable', 'Confirm', 'Whatif' - ) + $commonParams = [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + [System.Management.Automation.PSCmdlet]::CommonParameters $params | Where-Object { $_.Name -notin $commonParams } | Sort-Object -Property Name -Unique } @@ -16,12 +12,12 @@ BeforeDiscovery { $outputDir = Join-Path -Path $env:BHProjectPath -ChildPath 'Output' $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion - $outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" + $global:outputModVerManifest = Join-Path -Path $outputModVerDir -ChildPath "$($env:BHProjectName).psd1" # 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 - Import-Module -Name $outputModVerManifest -Verbose:$false -ErrorAction Stop + Import-Module -Name $global:outputModVerManifest -Verbose:$false -ErrorAction Stop -Force $params = @{ Module = (Get-Module $env:BHProjectName) CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias @@ -31,30 +27,39 @@ BeforeDiscovery { } $commands = Get-Command @params + Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore ## When testing help, remember that help is cached at the beginning of each session. ## To test, restart session. } +AfterAll { + Remove-Item Function:/FilterOutCommonParams +} + Describe "Test help for <_.Name>" -ForEach $commands { BeforeDiscovery { # Get command help, parameters, and links + Import-Module -Name $global:outputModVerManifest -Verbose:$false -ErrorAction Stop -Force $command = $_ - $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandHelp = Get-Help "$($env:BHProjectName)\$($command.Name)" -ErrorAction SilentlyContinue $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters $commandParameterNames = $commandParameters.Name $helpLinks = $commandHelp.relatedLinks.navigationLink.uri + Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore } BeforeAll { # These vars are needed in both discovery and test phases so we need to duplicate them here + Import-Module -Name $global:outputModVerManifest -Verbose:$false -ErrorAction Stop -Force $command = $_ $commandName = $_.Name - $commandHelp = Get-Help $command.Name -ErrorAction SilentlyContinue + $commandHelp = Get-Help "$($env:BHProjectName)\$($command.Name)" -ErrorAction SilentlyContinue $commandParameters = global:FilterOutCommonParams -Params $command.ParameterSets.Parameters $commandParameterNames = $commandParameters.Name $helpParameters = global:FilterOutCommonParams -Params $commandHelp.Parameters.Parameter $helpParameterNames = $helpParameters.Name + Get-Module $env:BHProjectName | Remove-Module -Force -ErrorAction Ignore } # If help is not found, synopsis in auto-generated help is the syntax diagram