Skip to content

Commit a4e5f76

Browse files
authored
Merge pull request #85 from coldbox-modules/filtering_nested_constraints
fix(ValidateOrFail): Handle filtering nested structs and arrays
2 parents a467517 + fad62bf commit a4e5f76

File tree

4 files changed

+205
-53
lines changed

4 files changed

+205
-53
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
fail-fast: false
2020
matrix:
21-
cfengine: [ "boxlang@1", "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2023", "adobe@2025" ]
21+
cfengine: [ "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2023", "adobe@2025" ]
2222
coldboxVersion: [ "^7.0.0" ]
2323
experimental: [ false ]
2424
# Experimental: ColdBox BE vs All Engines

models/ValidationManager.cfc

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,64 @@ component accessors="true" serialize="false" singleton {
302302
}
303303

304304
// Return validated keys
305-
return arguments.target.filter( function( key ){
306-
return constraints.keyExists( key );
307-
} );
305+
return filterTargetForConstraints( arguments.target, constraints );
306+
}
307+
308+
/**
309+
* Recursively filters the given target structure or object according to the provided constraints.
310+
*
311+
* This method processes the target and returns a new structure containing only the keys that exist in the constraints.
312+
* It handles nested constraints (via "constraints" or "nestedConstraints" keys) and array item constraints (via "items" or "arrayItem" keys)
313+
* by recursively filtering nested objects and arrays as needed.
314+
*
315+
* with nested structures and arrays filtered recursively as specified by the constraints.
316+
*
317+
* @target The target structure or object to filter. Can be a struct or an object containing fields to validate.
318+
* @constraints The structure of constraints to use for filtering the target. Keys correspond to fields in the target.
319+
*
320+
* @return struct: A new structure containing only the fields from the target that match the provided constraints,
321+
*/
322+
private any function filterTargetForConstraints( required any target, required struct constraints ){
323+
var filteredTarget = {};
324+
for ( var key in arguments.target ) {
325+
if ( !arguments.constraints.keyExists( key ) ) {
326+
continue;
327+
}
328+
329+
var constraint = arguments.constraints[ key ];
330+
if ( constraint.keyExists( "items" ) || constraint.keyExists( "arrayItem" ) ) {
331+
var filteredArray = [];
332+
var arrayConstraints = ( constraint.keyExists( "items" ) ? constraint.items : constraint.arrayItem );
333+
if ( arrayConstraints.keyExists( "constraints" ) || arrayConstraints.keyExists( "nestedConstraints" ) ) {
334+
for ( var item in arguments.target[ key ] ) {
335+
if ( isStruct( item ) ) {
336+
arrayAppend(
337+
filteredArray,
338+
filterTargetForConstraints(
339+
target = item,
340+
constraints = arrayConstraints.keyExists( "constraints" ) ? arrayConstraints.constraints : arrayConstraints.nestedConstraints
341+
)
342+
);
343+
} else {
344+
arrayAppend( filteredArray, item );
345+
}
346+
}
347+
} else {
348+
filteredArray = arguments.target[ key ];
349+
}
350+
filteredTarget[ key ] = filteredArray;
351+
} else if ( constraint.keyExists( "constraints" ) || constraint.keyExists( "nestedConstraints" ) ) {
352+
filteredTarget[ key ] = filterTargetForConstraints(
353+
target = arguments.target[ key ],
354+
constraints = (
355+
constraint.keyExists( "constraints" ) ? constraint.constraints : constraint.nestedConstraints
356+
)
357+
);
358+
} else {
359+
filteredTarget[ key ] = arguments.target[ key ];
360+
}
361+
}
362+
return filteredTarget;
308363
}
309364

310365
/**

test-harness/handlers/Main.cfc

Lines changed: 69 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ component {
55

66
// Index
77
any function index( event, rc, prc ){
8-
98
// Test Mixins
109
log.info( "validateHasValue #validateHasValue( "true" )# has passed!" );
1110
log.info( "validateIsNullOrEmpty #validateIsNullOrEmpty( "true" )# has passed!" );
1211
assert( true );
13-
try{
12+
try {
1413
assert( false, "bogus line" );
15-
} catch( AssertException e ){}
16-
catch( any e ){
14+
} catch ( AssertException e ) {
15+
} catch ( any e ) {
1716
rethrow;
1817
}
1918

@@ -26,39 +25,29 @@ component {
2625
password : { required : true, size : "6..20" }
2726
};
2827
// validation
29-
validate(
30-
target = rc,
31-
constraints = constraints
32-
).onError( function( results ){
33-
flash.put(
34-
"notice",
35-
arguments.results.getAllErrors().tostring()
36-
);
37-
return index( event, rc, prc );
38-
})
39-
.onSuccess( function( results ){
40-
flash.put( "notice", "User info validated!" );
41-
relocate( "main" );
42-
} )
28+
validate( target = rc, constraints = constraints )
29+
.onError( function( results ){
30+
flash.put( "notice", arguments.results.getAllErrors().tostring() );
31+
return index( event, rc, prc );
32+
} )
33+
.onSuccess( function( results ){
34+
flash.put( "notice", "User info validated!" );
35+
relocate( "main" );
36+
} )
4337
;
4438
}
4539

4640
any function saveShared( event, rc, prc ){
4741
// validation
48-
validate(
49-
target = rc,
50-
constraints = "sharedUser"
51-
).onError( function( results ){
52-
flash.put(
53-
"notice",
54-
results.getAllErrors().tostring()
55-
);
56-
return index( event, rc, prc );
57-
})
58-
.onSuccess( function( results ){
59-
flash.put( "User info validated!" );
60-
setNextEvent( "main" );
61-
} );
42+
validate( target = rc, constraints = "sharedUser" )
43+
.onError( function( results ){
44+
flash.put( "notice", results.getAllErrors().tostring() );
45+
return index( event, rc, prc );
46+
} )
47+
.onSuccess( function( results ){
48+
flash.put( "User info validated!" );
49+
setNextEvent( "main" );
50+
} );
6251
}
6352

6453
/**
@@ -71,10 +60,46 @@ component {
7160
};
7261

7362
// validate
74-
prc.keys = validateOrFail(
75-
target = rc,
76-
constraints = constraints
77-
);
63+
prc.keys = validateOrFail( target = rc, constraints = constraints );
64+
65+
return prc.keys;
66+
}
67+
68+
/**
69+
* validateOrFailWithNestedKeys
70+
*/
71+
function validateOrFailWithNestedKeys( event, rc, prc ){
72+
var constraints = {
73+
"keep0" : { "required" : true, "type" : "string" },
74+
"keepNested0" : {
75+
"required" : true,
76+
"type" : "struct",
77+
"constraints" : {
78+
"keepNested1" : {
79+
"required" : true,
80+
"type" : "struct",
81+
"constraints" : { "keep2" : { "required" : true, "type" : "string" } }
82+
},
83+
"keepArray1" : {
84+
"required" : true,
85+
"type" : "array",
86+
"items" : {
87+
"type" : "struct",
88+
"constraints" : { "keepNested3" : { "required" : true, "type" : "string" } }
89+
}
90+
},
91+
"keepArray1B" : {
92+
"required" : true,
93+
"type" : "array",
94+
"items" : { "type" : "array", "arrayItem" : { "type" : "string" } }
95+
}
96+
}
97+
},
98+
"keepNested0B.keep1B" : { "required" : true, "type" : "string" }
99+
};
100+
101+
// validate
102+
prc.keys = validateOrFail( target = rc, constraints = constraints );
78103

79104
return prc.keys;
80105
}
@@ -98,27 +123,22 @@ component {
98123
var oModel = populateModel( "User" );
99124

100125
// validate
101-
prc.object = validateOrFail(
102-
target = oModel,
103-
profiles = rc._profiles
104-
);
126+
prc.object = validateOrFail( target = oModel, profiles = rc._profiles );
105127

106128
return "Validated";
107-
}
108-
109-
110-
/**
129+
}
130+
131+
132+
/**
111133
* validateOnly
112134
*/
113-
function validateOnly( event, rc, prc){
114-
115-
var oModel = populateModel( "User" );
135+
function validateOnly( event, rc, prc ){
136+
var oModel = populateModel( "User" );
116137

117138
// validate
118-
prc.result = validate( oModel );
139+
prc.result = validate( oModel );
119140

120141
return "Validated";
121-
122142
}
123143

124144

test-harness/tests/specs/ValidationIntegrations.cfc

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,83 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
5959
.notToHaveKey( "anotherBogus" );
6060
} );
6161
} );
62+
63+
given( "valid nested data", function(){
64+
then( "it should give you back only the validated keys including in nested structs", function(){
65+
var e = this.request(
66+
route = "/main/validateOrFailWithNestedKeys",
67+
params = {
68+
"keepNested0" : {
69+
"keepNested1" : { "keep2" : "foo", "remove2" : "foo" },
70+
"keepArray1" : [
71+
{ "keepNested3" : "foo", "removeNested3" : "foo" },
72+
{ "keepNested3" : "bar", "removeNested3" : "bar" }
73+
],
74+
"keepArray1B" : [ [ "foo", "bar" ], [ "baz", "qux" ] ],
75+
"removeNested1" : { "foo" : "bar" },
76+
"remove1" : "foo"
77+
},
78+
"keepNested0B" : { "keep1B" : "foo", "remove1B" : "foo" },
79+
"keep0" : "foo",
80+
"remove0" : "foo"
81+
},
82+
method = "post"
83+
);
84+
85+
var keys = e.getPrivateValue( "keys" );
86+
debug( keys );
87+
expect( keys ).toBeStruct();
88+
expect( keys ).toHaveKey( "keepNested0" );
89+
expect( keys ).toHaveKey( "keepNested0B" );
90+
expect( keys ).toHaveKey( "keep0" );
91+
expect( keys ).notToHaveKey( "remove0" );
92+
93+
var nested0 = keys.keepNested0;
94+
expect( nested0 ).toBeStruct();
95+
expect( nested0 ).toHaveKey( "keepNested1" );
96+
expect( nested0 ).toHaveKey( "keepArray1" );
97+
expect( nested0 ).toHaveKey( "keepArray1B" );
98+
expect( nested0 ).notToHaveKey( "remove1" );
99+
expect( nested0 ).notToHaveKey( "removeNested1" );
100+
101+
var nested1 = nested0.keepNested1;
102+
expect( nested1 ).toBeStruct();
103+
expect( nested1 ).toHaveKey( "keep2" );
104+
expect( nested1 ).notToHaveKey( "remove2" );
105+
106+
var array1 = nested0.keepArray1;
107+
expect( array1 ).toBeArray();
108+
expect( array1 ).toHaveLength( 2 );
109+
expect( array1[ 1 ] ).toBeStruct();
110+
expect( array1[ 1 ] ).toHaveKey( "keepNested3" );
111+
expect( array1[ 1 ] ).notToHaveKey( "removeNested3" );
112+
expect( array1[ 2 ] ).toBeStruct();
113+
expect( array1[ 2 ] ).toHaveKey( "keepNested3" );
114+
expect( array1[ 2 ] ).notToHaveKey( "removeNested3" );
115+
116+
var array1B = nested0.keepArray1B;
117+
expect( array1B ).toBeArray();
118+
expect( array1B ).toHaveLength( 2 );
119+
expect( array1B[ 1 ] ).toBeArray();
120+
expect( array1B[ 1 ] ).toHaveLength( 2 );
121+
expect( array1B[ 1 ][ 1 ] ).toBeString();
122+
expect( array1B[ 1 ][ 1 ] ).toBe( "foo" );
123+
expect( array1B[ 1 ][ 2 ] ).toBeString();
124+
expect( array1B[ 1 ][ 2 ] ).toBe( "bar" );
125+
126+
expect( array1B[ 2 ] ).toBeArray();
127+
expect( array1B[ 2 ] ).toHaveLength( 2 );
128+
expect( array1B[ 2 ][ 1 ] ).toBeString();
129+
expect( array1B[ 2 ][ 1 ] ).toBe( "baz" );
130+
expect( array1B[ 2 ][ 2 ] ).toBeString();
131+
expect( array1B[ 2 ][ 2 ] ).toBe( "qux" );
132+
133+
var nested0B = keys.keepNested0B;
134+
expect( nested0B ).toBeStruct();
135+
expect( nested0B ).toHaveKey( "keep1B" );
136+
expect( nested0B ).notToHaveKey( "remove1B" );
137+
} );
138+
} );
62139
} );
63140

64141
story( "validate or fail with objects", function(){

0 commit comments

Comments
 (0)