diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1105aa3..37dcbbb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - cfengine: [ "boxlang@1", "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2023", "adobe@2025" ] + cfengine: [ "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2023", "adobe@2025" ] coldboxVersion: [ "^7.0.0" ] experimental: [ false ] # Experimental: ColdBox BE vs All Engines diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index e7dda24..34d65d8 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -302,9 +302,64 @@ component accessors="true" serialize="false" singleton { } // Return validated keys - return arguments.target.filter( function( key ){ - return constraints.keyExists( key ); - } ); + return filterTargetForConstraints( arguments.target, constraints ); + } + + /** + * Recursively filters the given target structure or object according to the provided constraints. + * + * This method processes the target and returns a new structure containing only the keys that exist in the constraints. + * It handles nested constraints (via "constraints" or "nestedConstraints" keys) and array item constraints (via "items" or "arrayItem" keys) + * by recursively filtering nested objects and arrays as needed. + * + * with nested structures and arrays filtered recursively as specified by the constraints. + * + * @target The target structure or object to filter. Can be a struct or an object containing fields to validate. + * @constraints The structure of constraints to use for filtering the target. Keys correspond to fields in the target. + * + * @return struct: A new structure containing only the fields from the target that match the provided constraints, + */ + private any function filterTargetForConstraints( required any target, required struct constraints ){ + var filteredTarget = {}; + for ( var key in arguments.target ) { + if ( !arguments.constraints.keyExists( key ) ) { + continue; + } + + var constraint = arguments.constraints[ key ]; + if ( constraint.keyExists( "items" ) || constraint.keyExists( "arrayItem" ) ) { + var filteredArray = []; + var arrayConstraints = ( constraint.keyExists( "items" ) ? constraint.items : constraint.arrayItem ); + if ( arrayConstraints.keyExists( "constraints" ) || arrayConstraints.keyExists( "nestedConstraints" ) ) { + for ( var item in arguments.target[ key ] ) { + if ( isStruct( item ) ) { + arrayAppend( + filteredArray, + filterTargetForConstraints( + target = item, + constraints = arrayConstraints.keyExists( "constraints" ) ? arrayConstraints.constraints : arrayConstraints.nestedConstraints + ) + ); + } else { + arrayAppend( filteredArray, item ); + } + } + } else { + filteredArray = arguments.target[ key ]; + } + filteredTarget[ key ] = filteredArray; + } else if ( constraint.keyExists( "constraints" ) || constraint.keyExists( "nestedConstraints" ) ) { + filteredTarget[ key ] = filterTargetForConstraints( + target = arguments.target[ key ], + constraints = ( + constraint.keyExists( "constraints" ) ? constraint.constraints : constraint.nestedConstraints + ) + ); + } else { + filteredTarget[ key ] = arguments.target[ key ]; + } + } + return filteredTarget; } /** diff --git a/test-harness/handlers/Main.cfc b/test-harness/handlers/Main.cfc index fdd2629..4b5911d 100644 --- a/test-harness/handlers/Main.cfc +++ b/test-harness/handlers/Main.cfc @@ -5,15 +5,14 @@ component { // Index any function index( event, rc, prc ){ - // Test Mixins log.info( "validateHasValue #validateHasValue( "true" )# has passed!" ); log.info( "validateIsNullOrEmpty #validateIsNullOrEmpty( "true" )# has passed!" ); assert( true ); - try{ + try { assert( false, "bogus line" ); - } catch( AssertException e ){} - catch( any e ){ + } catch ( AssertException e ) { + } catch ( any e ) { rethrow; } @@ -26,39 +25,29 @@ component { password : { required : true, size : "6..20" } }; // validation - validate( - target = rc, - constraints = constraints - ).onError( function( results ){ - flash.put( - "notice", - arguments.results.getAllErrors().tostring() - ); - return index( event, rc, prc ); - }) - .onSuccess( function( results ){ - flash.put( "notice", "User info validated!" ); - relocate( "main" ); - } ) + validate( target = rc, constraints = constraints ) + .onError( function( results ){ + flash.put( "notice", arguments.results.getAllErrors().tostring() ); + return index( event, rc, prc ); + } ) + .onSuccess( function( results ){ + flash.put( "notice", "User info validated!" ); + relocate( "main" ); + } ) ; } any function saveShared( event, rc, prc ){ // validation - validate( - target = rc, - constraints = "sharedUser" - ).onError( function( results ){ - flash.put( - "notice", - results.getAllErrors().tostring() - ); - return index( event, rc, prc ); - }) - .onSuccess( function( results ){ - flash.put( "User info validated!" ); - setNextEvent( "main" ); - } ); + validate( target = rc, constraints = "sharedUser" ) + .onError( function( results ){ + flash.put( "notice", results.getAllErrors().tostring() ); + return index( event, rc, prc ); + } ) + .onSuccess( function( results ){ + flash.put( "User info validated!" ); + setNextEvent( "main" ); + } ); } /** @@ -71,10 +60,46 @@ component { }; // validate - prc.keys = validateOrFail( - target = rc, - constraints = constraints - ); + prc.keys = validateOrFail( target = rc, constraints = constraints ); + + return prc.keys; + } + + /** + * validateOrFailWithNestedKeys + */ + function validateOrFailWithNestedKeys( event, rc, prc ){ + var constraints = { + "keep0" : { "required" : true, "type" : "string" }, + "keepNested0" : { + "required" : true, + "type" : "struct", + "constraints" : { + "keepNested1" : { + "required" : true, + "type" : "struct", + "constraints" : { "keep2" : { "required" : true, "type" : "string" } } + }, + "keepArray1" : { + "required" : true, + "type" : "array", + "items" : { + "type" : "struct", + "constraints" : { "keepNested3" : { "required" : true, "type" : "string" } } + } + }, + "keepArray1B" : { + "required" : true, + "type" : "array", + "items" : { "type" : "array", "arrayItem" : { "type" : "string" } } + } + } + }, + "keepNested0B.keep1B" : { "required" : true, "type" : "string" } + }; + + // validate + prc.keys = validateOrFail( target = rc, constraints = constraints ); return prc.keys; } @@ -98,27 +123,22 @@ component { var oModel = populateModel( "User" ); // validate - prc.object = validateOrFail( - target = oModel, - profiles = rc._profiles - ); + prc.object = validateOrFail( target = oModel, profiles = rc._profiles ); return "Validated"; - } - - - /** + } + + + /** * validateOnly */ - function validateOnly( event, rc, prc){ - - var oModel = populateModel( "User" ); + function validateOnly( event, rc, prc ){ + var oModel = populateModel( "User" ); // validate - prc.result = validate( oModel ); + prc.result = validate( oModel ); return "Validated"; - } diff --git a/test-harness/tests/specs/ValidationIntegrations.cfc b/test-harness/tests/specs/ValidationIntegrations.cfc index bb4274f..ebf5236 100644 --- a/test-harness/tests/specs/ValidationIntegrations.cfc +++ b/test-harness/tests/specs/ValidationIntegrations.cfc @@ -59,6 +59,83 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { .notToHaveKey( "anotherBogus" ); } ); } ); + + given( "valid nested data", function(){ + then( "it should give you back only the validated keys including in nested structs", function(){ + var e = this.request( + route = "/main/validateOrFailWithNestedKeys", + params = { + "keepNested0" : { + "keepNested1" : { "keep2" : "foo", "remove2" : "foo" }, + "keepArray1" : [ + { "keepNested3" : "foo", "removeNested3" : "foo" }, + { "keepNested3" : "bar", "removeNested3" : "bar" } + ], + "keepArray1B" : [ [ "foo", "bar" ], [ "baz", "qux" ] ], + "removeNested1" : { "foo" : "bar" }, + "remove1" : "foo" + }, + "keepNested0B" : { "keep1B" : "foo", "remove1B" : "foo" }, + "keep0" : "foo", + "remove0" : "foo" + }, + method = "post" + ); + + var keys = e.getPrivateValue( "keys" ); + debug( keys ); + expect( keys ).toBeStruct(); + expect( keys ).toHaveKey( "keepNested0" ); + expect( keys ).toHaveKey( "keepNested0B" ); + expect( keys ).toHaveKey( "keep0" ); + expect( keys ).notToHaveKey( "remove0" ); + + var nested0 = keys.keepNested0; + expect( nested0 ).toBeStruct(); + expect( nested0 ).toHaveKey( "keepNested1" ); + expect( nested0 ).toHaveKey( "keepArray1" ); + expect( nested0 ).toHaveKey( "keepArray1B" ); + expect( nested0 ).notToHaveKey( "remove1" ); + expect( nested0 ).notToHaveKey( "removeNested1" ); + + var nested1 = nested0.keepNested1; + expect( nested1 ).toBeStruct(); + expect( nested1 ).toHaveKey( "keep2" ); + expect( nested1 ).notToHaveKey( "remove2" ); + + var array1 = nested0.keepArray1; + expect( array1 ).toBeArray(); + expect( array1 ).toHaveLength( 2 ); + expect( array1[ 1 ] ).toBeStruct(); + expect( array1[ 1 ] ).toHaveKey( "keepNested3" ); + expect( array1[ 1 ] ).notToHaveKey( "removeNested3" ); + expect( array1[ 2 ] ).toBeStruct(); + expect( array1[ 2 ] ).toHaveKey( "keepNested3" ); + expect( array1[ 2 ] ).notToHaveKey( "removeNested3" ); + + var array1B = nested0.keepArray1B; + expect( array1B ).toBeArray(); + expect( array1B ).toHaveLength( 2 ); + expect( array1B[ 1 ] ).toBeArray(); + expect( array1B[ 1 ] ).toHaveLength( 2 ); + expect( array1B[ 1 ][ 1 ] ).toBeString(); + expect( array1B[ 1 ][ 1 ] ).toBe( "foo" ); + expect( array1B[ 1 ][ 2 ] ).toBeString(); + expect( array1B[ 1 ][ 2 ] ).toBe( "bar" ); + + expect( array1B[ 2 ] ).toBeArray(); + expect( array1B[ 2 ] ).toHaveLength( 2 ); + expect( array1B[ 2 ][ 1 ] ).toBeString(); + expect( array1B[ 2 ][ 1 ] ).toBe( "baz" ); + expect( array1B[ 2 ][ 2 ] ).toBeString(); + expect( array1B[ 2 ][ 2 ] ).toBe( "qux" ); + + var nested0B = keys.keepNested0B; + expect( nested0B ).toBeStruct(); + expect( nested0B ).toHaveKey( "keep1B" ); + expect( nested0B ).notToHaveKey( "remove1B" ); + } ); + } ); } ); story( "validate or fail with objects", function(){