Skip to content

Commit 1a8a4a3

Browse files
committed
Adding test cases and updating the core c# processor to properly handle multi level array validation
1 parent 06ba438 commit 1a8a4a3

11 files changed

Lines changed: 1110 additions & 138 deletions

File tree

.github/copilot-instructions.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copilot Instructions for PSFluentObjectValidation
2+
3+
## Project Overview
4+
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.
5+
6+
## Architecture
7+
8+
### Core Components
9+
- **C# Implementation**: `PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1` contains the main logic as embedded C# code using `Add-Type`
10+
- **Public Functions**: Thin PowerShell wrappers around the C# class
11+
- `Test-Exist`: Safe validation that returns boolean
12+
- `Assert-Exist`: Throws exceptions with detailed error messages
13+
- **Module Structure**: Standard PowerShell module with Public/Private folder separation
14+
15+
### Key Design Patterns
16+
17+
#### Validation Syntax
18+
The module uses a fluent dot notation with special operators:
19+
- `property.nested` - Basic navigation
20+
- `property!` - Non-empty validation (rejects null/empty/whitespace)
21+
- `property?` - Existence validation (allows null values)
22+
- `array[0]` - Array indexing
23+
- `array[*]` - Wildcard validation (all elements must pass)
24+
25+
#### Error Handling Strategy
26+
- `Test-Exist` wraps `Assert-Exist` in try/catch, never throws
27+
- `Assert-Exist` provides detailed error messages with context
28+
- C# implementation uses regex patterns for parsing validation operators
29+
30+
## Development Workflows
31+
32+
### Build System (psake + PowerShellBuild)
33+
```powershell
34+
# Bootstrap dependencies first (one-time setup)
35+
./build.ps1 -Bootstrap
36+
37+
# Standard development workflow
38+
./build.ps1 -Task Build # Compiles and validates module
39+
./build.ps1 -Task Test # Runs Pester tests + analysis
40+
./build.ps1 -Task Clean # Cleans output directory
41+
```
42+
43+
The build uses PowerShellBuild tasks defined in `psakeFile.ps1`. The `requirements.psd1` manages all build dependencies including Pester 5.4.0, PSScriptAnalyzer, and psake.
44+
45+
### Testing Strategy
46+
Tests live in `tests/` directory following these patterns:
47+
- `Manifest.tests.ps1` - Module manifest validation
48+
- `Meta.tests.ps1` - Code quality and PSScriptAnalyzer rules
49+
- `Help.tests.ps1` - Documentation validation
50+
- Use `ScriptAnalyzerSettings.psd1` for custom analysis rules
51+
52+
### Module Compilation
53+
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.
54+
55+
## Critical Implementation Details
56+
57+
### C# Embedded Code Patterns
58+
When modifying the C# implementation:
59+
- Use `Add-Type` with `ReferencedAssemblies` for System.Management.Automation
60+
- Regex patterns are compiled for performance: `PropertyWithValidation`, `ArrayIndexPattern`
61+
- Support multiple object types: Hashtables, PSObjects, .NET objects, Arrays, IList, IEnumerable
62+
63+
### Array Processing
64+
The `WildcardArrayWrapper` class enables wildcard validation by:
65+
1. Wrapping array objects during `[*]` processing
66+
2. Validating properties exist on ALL elements
67+
3. Returning first element's value for continued navigation
68+
69+
### Property Resolution Order
70+
1. Check for array indexing pattern `property[index]`
71+
2. Check for validation suffixes `property!` or `property?`
72+
3. Handle wildcard array wrapper context
73+
4. Fall back to regular property navigation
74+
75+
## Common Patterns
76+
77+
### Adding New Validation Operators
78+
1. Update regex patterns in C# code
79+
2. Add case handling in `ProcessPropertyWithValidation`
80+
3. Add validation logic in `ValidatePropertyValue`
81+
4. Update documentation and examples
82+
83+
### Testing Complex Object Structures
84+
Use the fluent syntax patterns from README.md:
85+
```powershell
86+
# Deep nesting with validation
87+
Test-Exist -In $data -With "users[0].profile.settings.theme!"
88+
89+
# Wildcard array validation
90+
Test-Exist -In $data -With "users[*].email!"
91+
92+
# Mixed indexing and wildcards
93+
Test-Exist -In $data -With "orders[1].items[*].price"
94+
```
95+
96+
### Error Message Conventions
97+
- Include property path context: `"Property 'user.name' does not exist"`
98+
- For arrays: `"Array index [10] is out of bounds for 'users' (length: 5)"`
99+
- For wildcards: `"Property 'email' in element [2] is empty"`
100+
101+
## Cross-Platform Considerations
102+
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.

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"windows": {
88
"options": {
99
"shell": {
10-
"executable": "powershell.exe",
10+
"executable": "pwsh.exe",
1111
"args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command" ]
1212
}
1313
}

CHANGELOG.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,32 @@
33
All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
6-
and this project adheres to [Semantic Versioning](http://semver.org/).
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.0.1] Released
8+
## [1.0.2] - 2025-09-24
9+
10+
### Added
11+
12+
- Added test cases
13+
14+
### Fixed
15+
16+
- Fixed a typo in Assert-Exists where the With alias was called Width
17+
- Fixed an issue with multi level array validation tests[*].users[1] was failing to properly validate before
18+
19+
### Changed
20+
21+
- Updated README
22+
- Updated CHANGELOG
23+
24+
## [1.0.1] - 2025-09-23
25+
26+
### Fixed
927

1028
- Fixing issue with powershell 5.1 compiling the c# code.
1129

12-
## [1.0.0] Released
30+
## [1.0.0] - 2025-09-23
31+
32+
### Added
1333

1434
- Initial release
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
@{
22
RootModule = 'PSFluentObjectValidation.psm1'
3-
ModuleVersion = '1.0.1'
3+
ModuleVersion = '1.0.2'
44
GUID = '90ac3c83-3bd9-4da5-8705-7b82b21963c8'
55
Author = 'Joshua Wilson'
66
CompanyName = 'PwshDevs'
77
Copyright = '(c) 2025 PwshDevs. All rights reserved.'
88
Description = 'Contains a helper class and functions to validate objects.'
99
PowerShellVersion = '5.1'
10-
FunctionsToExport = @('Test-Exists', 'Assert-Exists')
10+
FunctionsToExport = @('Test-Exist', 'Assert-Exist')
1111
CmdletsToExport = @()
1212
VariablesToExport = '*'
1313
AliasesToExport = @('exists', 'asserts', 'tests')
1414
PrivateData = @{
1515
PSData = @{
16-
Tags = @('Validation', 'Object', 'Fluent', 'Helper', 'Assert', 'Test', 'Exists')
16+
Tags = @('PSEdition_Desktop', 'PSEdition_Core', 'Windows', 'Linux', 'MacOS', 'Validation', 'Object', 'Fluent', 'Helper', 'Assert', 'Test', 'Exists')
1717
LicenseUri = 'https://github.com/pwshdevs/PSFluentObjectValidation/blob/main/LICENSE'
1818
ProjectUri = 'https://github.com/pwshdevs/PSFluentObjectValidation'
19-
ReleaseNotes = ''
19+
ReleaseNotes = 'https://github.com/pwshdevs/PSFluentObjectValidation/blob/main/CHANGELOG.md'
2020
}
2121
}
2222
}

PSFluentObjectValidation/Private/PSFluentObjectValidation.ps1

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static class PSFluentObjectValidation
2828
if (inputObject == null)
2929
throw new ArgumentException("InputObject cannot be null");
3030
31-
if (string.IsNullOrEmpty(key))
31+
if (String.IsNullOrEmpty(key))
3232
throw new ArgumentException("Key cannot be null or empty");
3333
3434
string[] keyParts = key.Split('.');
@@ -42,6 +42,13 @@ public static class PSFluentObjectValidation
4242
4343
private static object ProcessKeyPart(object currentObject, string part)
4444
{
45+
// Handle wildcard array wrapper specially
46+
if (currentObject is WildcardArrayWrapper)
47+
{
48+
WildcardArrayWrapper wrapper = (WildcardArrayWrapper)currentObject;
49+
return ProcessWildcardPropertyAccess(wrapper.ArrayObject, part);
50+
}
51+
4552
// Check for array indexing: property[index] or property[*]
4653
Match arrayMatch = ArrayIndexPattern.Match(part);
4754
if (arrayMatch.Success)
@@ -225,6 +232,112 @@ public static class PSFluentObjectValidation
225232
226233
private static object ProcessWildcardPropertyAccess(object arrayObject, string propertyName)
227234
{
235+
// First check if this is an array indexing pattern: property[index] or property[*]
236+
Match arrayMatch = ArrayIndexPattern.Match(propertyName);
237+
if (arrayMatch.Success)
238+
{
239+
string basePropertyName = arrayMatch.Groups[1].Value;
240+
string indexStr = arrayMatch.Groups[2].Value;
241+
242+
// Handle array indexing after wildcard: items[0], tags[*], etc.
243+
if (arrayObject is Array)
244+
{
245+
Array array = (Array)arrayObject;
246+
for (int i = 0; i < array.Length; i++)
247+
{
248+
object element = array.GetValue(i);
249+
if (element == null)
250+
throw new InvalidOperationException(String.Format("Array element [{0}] is null", i));
251+
252+
if (!HasProperty(element, basePropertyName))
253+
throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName));
254+
255+
object propertyValue = GetProperty(element, basePropertyName);
256+
if (propertyValue == null)
257+
throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i));
258+
if (!IsArrayLike(propertyValue))
259+
throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i));
260+
}
261+
262+
// All elements are valid, now handle the indexing
263+
object firstElement = array.GetValue(0);
264+
object firstPropertyValue = GetProperty(firstElement, basePropertyName);
265+
266+
if (indexStr == "*")
267+
{
268+
return new WildcardArrayWrapper(firstPropertyValue);
269+
}
270+
else
271+
{
272+
int index = int.Parse(indexStr);
273+
int count = GetCount(firstPropertyValue);
274+
if (index < 0 || index >= count)
275+
throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count));
276+
277+
if (firstPropertyValue is Array)
278+
{
279+
Array firstArray = (Array)firstPropertyValue;
280+
return firstArray.GetValue(index);
281+
}
282+
if (firstPropertyValue is IList)
283+
{
284+
IList firstList = (IList)firstPropertyValue;
285+
return firstList[index];
286+
}
287+
}
288+
}
289+
290+
if (arrayObject is IList)
291+
{
292+
IList list = (IList)arrayObject;
293+
for (int i = 0; i < list.Count; i++)
294+
{
295+
object element = list[i];
296+
if (element == null)
297+
throw new InvalidOperationException(String.Format("Array element [{0}] is null", i));
298+
if (!HasProperty(element, basePropertyName))
299+
throw new InvalidOperationException(String.Format("Array element [{0}] does not have property '{1}'", i, basePropertyName));
300+
301+
object propertyValue = GetProperty(element, basePropertyName);
302+
if (propertyValue == null)
303+
throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is null", basePropertyName, i));
304+
if (!IsArrayLike(propertyValue))
305+
throw new InvalidOperationException(String.Format("Property '{0}' in element [{1}] is not an array", basePropertyName, i));
306+
}
307+
308+
// All elements are valid, now handle the indexing
309+
object firstElement = list[0];
310+
object firstPropertyValue = GetProperty(firstElement, basePropertyName);
311+
312+
if (indexStr == "*")
313+
{
314+
return new WildcardArrayWrapper(firstPropertyValue);
315+
}
316+
else
317+
{
318+
int index = int.Parse(indexStr);
319+
int count = GetCount(firstPropertyValue);
320+
if (index < 0 || index >= count)
321+
throw new InvalidOperationException(String.Format("Array index [{0}] is out of bounds for property '{1}' (length: {2})", index, basePropertyName, count));
322+
323+
if (firstPropertyValue is Array)
324+
{
325+
Array firstArray = (Array)firstPropertyValue;
326+
return firstArray.GetValue(index);
327+
}
328+
if (firstPropertyValue is IList)
329+
{
330+
IList firstList = (IList)firstPropertyValue;
331+
return firstList[index];
332+
}
333+
}
334+
}
335+
336+
throw new InvalidOperationException(String.Format("Cannot process wildcard array indexing on type {0}", arrayObject.GetType().Name));
337+
}
338+
339+
340+
228341
// Parse validation suffix if present
229342
Match validationMatch = PropertyWithValidation.Match(propertyName);
230343
string actualPropertyName = propertyName;

PSFluentObjectValidation/Public/Assert-Exist.ps1

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
<#
2+
.SYNOPSIS
3+
Asserts the existence and validity of a property within an object.
4+
5+
.DESCRIPTION
6+
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.
7+
8+
.PARAMETER InputObject
9+
The object to validate. This can be a hashtable, PSObject, .NET object, or any other object type.
10+
11+
.PARAMETER Key
12+
The property path to validate. Supports fluent syntax with validation operators:
13+
- `property.nested` - Basic navigation
14+
- `property!` - Non-empty validation (rejects null/empty/whitespace)
15+
- `property?` - Existence validation (allows null values)
16+
- `array[0]` - Array indexing
17+
- `array[*]` - Wildcard validation (all elements must pass)
18+
19+
.EXAMPLE
20+
# Validate that the `user.name` property exists and is non-empty
21+
Assert-Exist -InputObject $data -Key "user.name!"
22+
23+
.EXAMPLE
24+
# Validate that all users in the array have a non-empty email
25+
Assert-Exist -InputObject $data -Key "users[*].email!"
26+
27+
.EXAMPLE
28+
# Validate that the `settings.theme` property exists
29+
Assert-Exist -InputObject $data -Key "settings.theme"
30+
31+
.NOTES
32+
Throws an exception if the validation fails. Use `Test-Exist` for a non-throwing alternative.
33+
34+
.LINK
35+
https://www.pwshdevs.com/
36+
#>
137
function Assert-Exist {
238
param(
339
[Parameter(Mandatory=$true)]

PSFluentObjectValidation/Public/Test-Exist.ps1

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
<#
2+
.SYNOPSIS
3+
Tests the existence and validity of a property within an object.
4+
5+
.DESCRIPTION
6+
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.
7+
8+
.PARAMETER InputObject
9+
The object to validate. This can be a hashtable, PSObject, .NET object, or any other object type.
10+
11+
.PARAMETER Key
12+
The property path to validate. Supports fluent syntax with validation operators:
13+
- `property.nested` - Basic navigation
14+
- `property!` - Non-empty validation (rejects null/empty/whitespace)
15+
- `property?` - Existence validation (allows null values)
16+
- `array[0]` - Array indexing
17+
- `array[*]` - Wildcard validation (all elements must pass)
18+
19+
.EXAMPLE
20+
# Test if the `user.name` property exists and is non-empty
21+
Test-Exist -InputObject $data -Key "user.name!"
22+
23+
.EXAMPLE
24+
# Test if all users in the array have a non-empty email
25+
Test-Exist -InputObject $data -Key "users[*].email!"
26+
27+
.EXAMPLE
28+
# Test if the `settings.theme` property exists
29+
Test-Exist -InputObject $data -Key "settings.theme"
30+
31+
.NOTES
32+
Returns `$true` if the validation passes, `$false` otherwise. Use `Assert-Exist` for a throwing alternative.
33+
34+
.LINK
35+
https://www.pwshdevs.com/
36+
#>
137
Function Test-Exist {
238
param(
339
[Parameter(Mandatory=$true)]

0 commit comments

Comments
 (0)