diff --git a/.github/CODE_OF_CONDUCT.MD b/.github/CODE_OF_CONDUCT.MD new file mode 100644 index 0000000..12507ab --- /dev/null +++ b/.github/CODE_OF_CONDUCT.MD @@ -0,0 +1,3 @@ +# Code of Conduct + +Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#code-of-conduct). diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..f057099 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#security-vulnerabilities). diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..3bb8adb --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,3 @@ +# Support & Help + +Please see it in our [Contributing Guidelines](../CONTRIBUTING.md#support-questions). diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d78c2d7..b0bf721 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -6,10 +6,8 @@ on: - "main" - "master" - "development" - - "releases/v*" pull_request: branches: - - "releases/v*" - development jobs: @@ -17,12 +15,13 @@ jobs: uses: ./.github/workflows/tests.yml secrets: inherit - formatCheck: + # Format PR + format_check: name: Checks Source Code Formatting - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: Ortus-Solutions/commandbox-action@v1.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6894a7..1e5da4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ on: type: boolean env: - MODULE_ID: cbvalidation + MODULE_ID: ${{ github.event.repository.name }} SNAPSHOT: ${{ inputs.snapshot || false }} jobs: @@ -26,10 +26,10 @@ jobs: ########################################################################################## build: name: Build & Publish - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup CommandBox uses: Ortus-Solutions/setup-commandbox@v2.0.1 @@ -64,7 +64,7 @@ jobs: box task run taskfile=build/Build target=run :version=${{ env.VERSION }} :projectName=${{ env.MODULE_ID }} :buildID=${{ github.run_number }} :branch=${{ env.BRANCH }} - name: Commit Changelog To Master - uses: EndBug/add-and-commit@v9.1.1 + uses: EndBug/add-and-commit@v9.1.4 if: env.SNAPSHOT == 'false' with: author_name: Github Actions @@ -73,7 +73,7 @@ jobs: add: changelog.md - name: Tag Version - uses: rickstaa/action-create-tag@v1.6.1 + uses: rickstaa/action-create-tag@v1.7.2 if: env.SNAPSHOT == 'false' with: tag: "v${{ env.VERSION }}" @@ -82,7 +82,7 @@ jobs: - name: Upload Build Artifacts if: success() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.MODULE_ID }} path: | @@ -118,7 +118,7 @@ jobs: box forgebox publish --force - name: Create Github Release - uses: taiki-e/create-gh-release-action@v1.6.2 + uses: taiki-e/create-gh-release-action@v1.8.2 continue-on-error: true if: env.SNAPSHOT == 'false' with: @@ -133,12 +133,12 @@ jobs: prep_next_release: name: Prep Next Release if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [ build ] steps: # Checkout development - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: development @@ -148,7 +148,7 @@ jobs: forgeboxAPIKey: ${{ secrets.FORGEBOX_TOKEN }} - name: Download build artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: ${{ env.MODULE_ID }} path: .tmp @@ -165,7 +165,7 @@ jobs: # Commit it back to development - name: Commit Version Bump - uses: EndBug/add-and-commit@v9.1.1 + uses: EndBug/add-and-commit@v9.1.4 with: author_name: Github Actions author_email: info@ortussolutions.com diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 45d7dd1..7f651fa 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -5,6 +5,13 @@ on: branches: - 'development' + workflow_dispatch: + +# Unique group name per workflow-branch/tag combo +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: ########################################################################################## # Module Tests @@ -18,9 +25,9 @@ jobs: ########################################################################################## format: name: Code Auto-Formatting - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Auto-format uses: Ortus-Solutions/commandbox-action@v1.0.2 @@ -28,7 +35,7 @@ jobs: cmd: run-script format - name: Commit Format Changes - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Apply cfformat changes diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d10f11..b3de1fc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ on: jobs: tests: name: Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: DB_USER: root DB_PASSWORD: root @@ -18,58 +18,38 @@ jobs: strategy: fail-fast: false matrix: - cfengine: [ "lucee@5", "adobe@2018", "adobe@2021" ] - coldboxVersion: [ "^6.0.0" ] + cfengine: [ "boxlang-cfml@1", "lucee@5", "lucee@6", "adobe@2021", "adobe@2023" ] + coldboxVersion: [ "^7.0.0" ] experimental: [ false ] include: - - cfengine: "adobe@2023" - coldboxVersion: "^6.0.0" - experimental: true - coldboxVersion: "be" cfengine: "lucee@5" experimental: true - coldboxVersion: "be" - cfengine: "adobe@2018" + cfengine: "lucee@6" experimental: true - coldboxVersion: "be" cfengine: "adobe@2021" experimental: true + - coldboxVersion: "be" + cfengine: "adobe@2023" + experimental: true + - coldboxVersion: "be" + cfengine: "boxlang-cfml@1" + experimental: true steps: - name: Checkout Repository - uses: actions/checkout@v3 - - # Not Needed in this module - #- name: Setup Database and Fixtures - # run: | - # sudo /etc/init.d/mysql start - # mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} -e 'CREATE DATABASE coolblog;' - # mysql -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }} < test-harness/tests/resources/coolblog.sql + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: "temurin" - java-version: "11" + java-version: "21" - name: Setup CommandBox CLI uses: Ortus-Solutions/setup-commandbox@v2.0.1 - # Not Needed in this module - #- name: Setup Environment For Testing Process - # working-directory: ./test-harness - # run: | - # # Setup .env - # touch .env - # # ENV - # printf "DB_HOST=localhost\n" >> .env - # printf "DB_DATABASE=mydatabase\n" >> .env - # printf "DB_DRIVER=MySQL\n" >> .env - # printf "DB_USER=${{ env.DB_USER }}\n" >> .env - # printf "DB_PASSWORD=${{ env.DB_PASSWORD }}\n" >> .env - # printf "DB_CLASS=com.mysql.cj.jdbc.Driver\n" >> .env - # printf "DB_BUNDLEVERSION=8.0.19\n" >> .env - # printf "DB_BUNDLENAME=com.mysql.cj\n" >> .env - - name: Install Test Harness with ColdBox ${{ matrix.coldboxVersion }} run: | box install @@ -96,7 +76,7 @@ jobs: - name: Upload Test Results to Artifacts if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.cfengine }}-${{ matrix.coldboxVersion }} path: | @@ -109,7 +89,7 @@ jobs: - name: Upload Debug Logs To Artifacts if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Failure Debugging Info - ${{ matrix.cfengine }} - ${{ matrix.coldboxVersion }} path: | diff --git a/.markdownlint.json b/.markdownlint.json index 31705fa..26ea402 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -10,5 +10,9 @@ }, "no-duplicate-header" : { "siblings_only" : true - } -} \ No newline at end of file + }, + "no-duplicate-heading" : { + "siblings_only" : true + }, + "no-inline-html" : false +} diff --git a/box.json b/box.json index 5eab7d6..f3498b1 100644 --- a/box.json +++ b/box.json @@ -1,7 +1,7 @@ { "name":"ColdBox Validation", "author":"Ortus Solutions ", - "version":"4.4.0", + "version":"4.5.0", "location":"https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/cbvalidation/@build.version@/cbvalidation-@build.version@.zip", "slug":"cbvalidation", "type":"modules", @@ -26,6 +26,7 @@ "cbi18n":"^3.0.0" }, "devDependencies":{ + "commandbox-boxlang":"*", "commandbox-cfformat":"*", "commandbox-docbox":"*", "commandbox-dotenv":"*", @@ -58,6 +59,5 @@ "logs:2021":"server log serverConfigFile=server-adobe@2021.json --follow" }, "installPaths":{ - "cbi18n":"modules/cbi18n/" } } diff --git a/build/Build.cfc b/build/Build.cfc index ee9366e..b15a671 100644 --- a/build/Build.cfc +++ b/build/Build.cfc @@ -12,6 +12,7 @@ component { variables.cwd = getCWD().reReplace( "\.$", "" ); variables.artifactsDir = cwd & "/.artifacts"; variables.buildDir = cwd & "/.tmp"; + variables.apidDocsDir = variables.buildDir & "/apidocs"; variables.apiDocsURL = "http://localhost:60299/apidocs/"; variables.testRunner = "http://localhost:60299/tests/runner.cfm"; @@ -31,7 +32,8 @@ component { // Cleanup + Init Build Directories [ variables.buildDir, - variables.artifactsDir + variables.artifactsDir, + variables.apidDocsDir ].each( function( item ){ if ( directoryExists( item ) ) { directoryDelete( item, true ); diff --git a/changelog.md b/changelog.md index afc6276..6358cd8 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- `eurodate` doesn't event work in ACF/Lucee as it uses just a single standard. Remove it so the user can validate as they see fit. + +### Added + +- BoxLang certification +- Lucee 6, Adobe 2023 +- Add `defaultValue` to apply default values before constraints are checked + ## [4.4.0] - 2023-10-16 ### Added diff --git a/models/GenericObject.cfc b/models/GenericObject.cfc index a3f4e7e..78e5dbe 100644 --- a/models/GenericObject.cfc +++ b/models/GenericObject.cfc @@ -30,14 +30,34 @@ component { * @missingMethodName * @missingMethodArguments */ - any function onMissingMethod( required string missingMethodName, required struct missingMethodArguments ){ - var key = replaceNoCase( arguments.missingMethodName, "get", "" ); - - if ( structKeyExists( variables.collection, key ) ) { + any function onMissingMethod( required string missingMethodName, required any missingMethodArguments ){ + if ( startsWith( arguments.missingMethodName, "set" ) ) { + var key = mid( + arguments.missingMethodName, + 4, + len( arguments.missingMethodName ) - 3 + ); + variables.collection[ key ] = arguments.missingMethodArguments[ 1 ]; return variables.collection[ key ]; } + if ( startsWith( arguments.missingMethodName, "get" ) ) { + var key = mid( + arguments.missingMethodName, + 4, + len( arguments.missingMethodName ) - 3 + ); + if ( structKeyExists( variables.collection, key ) ) { + return variables.collection[ key ]; + } + } + // Return null + return javacast( "null", "" ); + } + + private boolean function startsWith( word, substring ){ + return left( word, len( substring ) ) == substring; } } diff --git a/models/ValidationManager.cfc b/models/ValidationManager.cfc index 380cd56..1c6c298 100644 --- a/models/ValidationManager.cfc +++ b/models/ValidationManager.cfc @@ -18,7 +18,7 @@ * // required or not * required : boolean [false] * // type constraint - * type : (ssn,email,url,alpha,boolean,date,usdate,eurodate,numeric,GUID,UUID,integer,[string],telephone,zipcode,ipaddress,creditcard,binary,component,query,struct,json,xml), + * type : (ssn,email,url,alpha,boolean,date,usdate,numeric,GUID,UUID,integer,[string],telephone,zipcode,ipaddress,creditcard,binary,component,query,struct,json,xml), * // size or length of the value (struct,string,array,query) * size : numeric or range, eg: 10 or 6..8 * // range is a range of values the property value should exist in @@ -212,6 +212,27 @@ component accessors="true" serialize="false" singleton { arguments.includeFields = arrayToList( arguments.includeFields ); } + // iterate over fields to apply default values + for ( var thisField in allConstraints ) { + /* + results = results, + rules = allConstraints[ thisField ], + target = arguments.target, + field = thisField, + locale = arguments.locale + */ + if ( allConstraints[ thisField ].keyExists( "defaultValue" ) ) { + var targetValue = invoke( arguments.target, "get" & thisField ); + if ( isNull( targetValue ) || !hasValue( targetValue ) ) { + invoke( + arguments.target, + "set" & thisField, + [ allConstraints[ thisField ].defaultValue ] + ); + } + } + } + // iterate over constraints defined for ( var thisField in allConstraints ) { var validateField = true; @@ -304,7 +325,7 @@ component accessors="true" serialize="false" singleton { // process the incoming rules for ( var key in arguments.rules ) { // if message validators, just ignore - if ( reFindNoCase( "Message$", key ) ) { + if ( reFindNoCase( "Message$", key ) || key == "defaultValue" ) { continue; } @@ -507,4 +528,36 @@ component accessors="true" serialize="false" singleton { ); } + /** + * Verify if the target value has a value + * Checks for nullness or for length if it's a simple value, array, query, struct or object. + */ + boolean function hasValue( any targetValue ){ + // Null Tests + if ( isNull( arguments.targetValue ) ) { + return false; + } + // Simple Tests + if ( isSimpleValue( arguments.targetValue ) AND len( trim( arguments.targetValue ) ) ) { + return true; + } + // Array Tests + if ( isArray( arguments.targetValue ) and arrayLen( arguments.targetValue ) ) { + return true; + } + // Query Tests + if ( isQuery( arguments.targetValue ) and arguments.targetValue.recordcount ) { + return true; + } + // Struct Tests + if ( isStruct( arguments.targetValue ) and structCount( arguments.targetValue ) ) { + return true; + } + // Object + if ( isObject( arguments.targetValue ) ) { + return true; + } + return false; + } + } diff --git a/models/validators/TypeValidator.cfc b/models/validators/TypeValidator.cfc index 53a4ba8..fbf478c 100644 --- a/models/validators/TypeValidator.cfc +++ b/models/validators/TypeValidator.cfc @@ -11,7 +11,7 @@ component extends="BaseValidator" accessors="true" singleton { */ TypeValidator function init(){ variables.name = "Type"; - variables.validTypes = "alpha,array,binary,boolean,component,creditcard,date,email,eurodate,float,GUID,integer,ipaddress,json,numeric,query,ssn,string,struct,telephone,url,usdate,UUID,xml,zipcode"; + variables.validTypes = "alpha,array,binary,boolean,component,creditcard,date,email,float,GUID,integer,ipaddress,json,numeric,query,ssn,string,struct,telephone,url,usdate,UUID,xml,zipcode"; return this; } @@ -83,10 +83,6 @@ component extends="BaseValidator" accessors="true" singleton { r = isValid( "usdate", arguments.targetValue ); break; } - case "eurodate": { - r = isValid( "eurodate", arguments.targetValue ); - break; - } case "numeric": { r = isValid( "numeric", arguments.targetValue ); break; diff --git a/readme.md b/readme.md index 1c2bffd..aa52624 100644 --- a/readme.md +++ b/readme.md @@ -13,26 +13,25 @@ www.ortussolutions.com

----- +# Welcome to the Validation Module for ColdBox -# WELCOME TO THE COLDBOX VALIDATION MODULE +This module is a server side rules validation engine that can provide you with a unified approach to object, struct and form validation. You can construct validation constraint rules and then tell the engine to validate them accordingly. You can validate objects or structures and even use profiles to target specific fields for validation. The validation engine is highly extensible and can be used in any kind of application. -This module is a server side rules validation engine that can provide you with a unified approach to object, struct and form validation. You can construct validation constraint rules and then tell the engine to validate them accordingly. - -## LICENSE +## License Apache License, Version 2.0. -## IMPORTANT LINKS +## Important Links - https://github.com/coldbox-modules/cbvalidation - https://coldbox-validation.ortusbooks.com - https://forgebox.io/view/cbvalidation -## SYSTEM REQUIREMENTS +## Requirements +- BoxLang 1+ - Lucee 5.x+ -- Adobe ColdFusion 2018+ +- Adobe ColdFusion 2021+ ## Installation @@ -212,7 +211,7 @@ this.constraints = { size : numeric or range, eg: 10 or 6..8 // specific type constraint, one in the list. - type : (alpha,array,binary,boolean,component,creditcard,date,email,eurodate,float,GUID,integer,ipaddress,json,numeric,query,ssn,string,struct,telephone,url,usdate,UUID,xml,zipcode), + type : (alpha,array,binary,boolean,component,creditcard,date,email,float,GUID,integer,ipaddress,json,numeric,query,ssn,string,struct,telephone,url,usdate,UUID,xml,zipcode), // UDF to use for validation, must return boolean accept the incoming value and target object, validate(value,target):boolean udf = variables.UDF or this.UDF or a closure. diff --git a/server-adobe@2023.json b/server-adobe@2023.json index c5fce67..d7d7330 100644 --- a/server-adobe@2023.json +++ b/server-adobe@2023.json @@ -2,7 +2,7 @@ "name":"cbvalidation-adobe@2023", "app":{ "serverHomeDirectory":".engine/adobe2023", - "cfengine":"adobe@2023.0.0-beta.1" + "cfengine":"adobe@2023" }, "web":{ "http":{ @@ -11,19 +11,19 @@ "rewrites":{ "enable":"true" }, - "webroot": "test-harness", - "aliases":{ - "/moduleroot/cbvalidation":"../" + "webroot":"test-harness", + "aliases":{ + "/moduleroot/cbvalidation":"./" } }, - "jvm":{ + "jvm":{ "heapSize":"1024" }, "openBrowser":"false", "cfconfig": { "file" : ".cfconfig.json" }, - "scripts" : { + "scripts" : { "onServerInstall":"cfpm install zip,debugger" } } diff --git a/server-boxlang-cfml@1.json b/server-boxlang-cfml@1.json new file mode 100644 index 0000000..bbac0d1 --- /dev/null +++ b/server-boxlang-cfml@1.json @@ -0,0 +1,36 @@ +{ + "app":{ + "cfengine":"boxlang@be", + "serverHomeDirectory":".engine/boxlang" + }, + "name":"cbvalidation-boxlang@1", + "force":true, + "openBrowser":false, + "web":{ + "directoryBrowsing":true, + "http":{ + "port":"60299" + }, + "rewrites":{ + "enable":"true" + }, + "webroot":"test-harness", + "aliases":{ + "/moduleroot/cbvalidation":"./" + } + }, + "JVM":{ + "heapSize":"1024", + "javaVersion":"openjdk21_jre", + "args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888 -Dboxlang.debugMode=true" + }, + "cfconfig":{ + "file":".cfconfig.json" + }, + "env":{ + "BOXLANG_DEBUG":true + }, + "scripts":{ + "onServerInitialInstall":"install bx-compat-cfml --noSave" + } +} diff --git a/server-lucee@6.json b/server-lucee@6.json new file mode 100644 index 0000000..a6f08e7 --- /dev/null +++ b/server-lucee@6.json @@ -0,0 +1,23 @@ +{ + "name":"cbi18n-lucee@6", + "app":{ + "serverHomeDirectory":".engine/lucee6", + "cfengine":"lucee@6" + }, + "web":{ + "http":{ + "port":"60299" + }, + "rewrites":{ + "enable":"true" + }, + "webroot":"test-harness", + "aliases":{ + "/moduleroot/cbi18n":"../" + } + }, + "openBrowser":"false", + "cfconfig":{ + "file":".cfconfig.json" + } +} diff --git a/test-harness/tests/resources/ValidationManager.cfc b/test-harness/tests/resources/ValidationManager.cfc index 1dec46a..23c2fe4 100644 --- a/test-harness/tests/resources/ValidationManager.cfc +++ b/test-harness/tests/resources/ValidationManager.cfc @@ -18,7 +18,7 @@ * // required or not * required : boolean [false] * // type constraint - * type : (ssn,email,url,alpha,boolean,date,usdate,eurodate,numeric,GUID,UUID,integer,[string],telephone,zipcode,ipaddress,creditcard,binary,component,query,struct,json,xml), + * type : (ssn,email,url,alpha,boolean,date,usdate,numeric,GUID,UUID,integer,[string],telephone,zipcode,ipaddress,creditcard,binary,component,query,struct,json,xml), * // size or length of the value (struct,string,array,query) * size : numeric or range, eg: 10 or 6..8 * // range is a range of values the property value should exist in diff --git a/test-harness/tests/specs/ValidationManagerTest.cfc b/test-harness/tests/specs/ValidationManagerTest.cfc index d2e89e6..182080f 100644 --- a/test-harness/tests/specs/ValidationManagerTest.cfc +++ b/test-harness/tests/specs/ValidationManagerTest.cfc @@ -106,6 +106,17 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" { debug( r.getAllErrors() ); } ); + it( "can apply default values before validating", function(){ + var mockData = { name : "luis" }; + var mockConstraints = { + name : { required : true }, + age : { required : true, max : 35, defaultValue : 30 } + }; + + var r = manager.validate( target = mockData, constraints = mockConstraints ); + assertEquals( false, r.hasErrors() ); + } ); + it( "can validate with specific fields", function(){ var mockData = { name : "", age : "" }; var mockConstraints = { diff --git a/test-harness/tests/specs/validators/TypeValidatorTest.cfc b/test-harness/tests/specs/validators/TypeValidatorTest.cfc index f52b108..e4f8915 100644 --- a/test-harness/tests/specs/validators/TypeValidatorTest.cfc +++ b/test-harness/tests/specs/validators/TypeValidatorTest.cfc @@ -66,12 +66,6 @@ component extends="coldbox.system.testing.BaseModelTest" model="cbvalidation.mod r = model.validate( result, this, "test", "01/01/2012", "usdate" ); assertEquals( true, r ); - // eurodate - r = model.validate( result, this, "test", "1aa", "eurodate" ); - assertEquals( false, r ); - r = model.validate( result, this, "test", "23/01/2012", "eurodate" ); - assertEquals( true, r ); - // numeric r = model.validate( result, this, "test", "1aa", "numeric" ); assertEquals( false, r );