From 1319a3d7436660df581fade4ff2ec866e4e9de66 Mon Sep 17 00:00:00 2001 From: davebaol Date: Mon, 10 Jun 2019 09:58:36 +0200 Subject: [PATCH 01/16] Bye bye $path Now validators need a scope in input to run validation. Reference $path has gone since the object to validate can be referenced by name $ through $var, just like regular variables. In fact $ is a property of the context shared by any scope. --- TODO.md | 114 ++++++++++------ examples/data-driven.js | 3 +- examples/dsl-validator.js | 3 +- examples/hard-coded.js | 3 +- src/branch-validators.js | 199 ++++++++++++++++------------ src/leaf-validators/bridge.js | 9 +- src/leaf-validators/index.js | 51 ++++--- src/util/argument.js | 4 +- src/util/context.js | 15 ++- src/util/create-shortcuts.js | 7 +- src/util/expression.js | 26 +--- src/util/info.js | 4 +- src/util/misc.js | 3 + src/util/scope.js | 31 +++-- src/util/types.js | 11 +- test/branch-validators/def.js | 15 ++- test/branch-validators/if.js | 11 +- test/branch-validators/iterators.js | 20 +-- test/branch-validators/misc.js | 9 +- test/leaf-validators/equals.js | 6 +- test/leaf-validators/isLength.js | 7 +- test/test-utils.js | 73 ++++------ test/util/context.js | 2 +- test/util/create-shortcuts.js | 9 +- test/util/expression.js | 31 ++--- test/util/scope.js | 14 +- test/util/types/ref.js | 18 +-- 27 files changed, 370 insertions(+), 328 deletions(-) diff --git a/TODO.md b/TODO.md index 39b1ca2..8bb5dc2 100644 --- a/TODO.md +++ b/TODO.md @@ -1,67 +1,101 @@ New ideas and improvements for future releases ============================================== -[0] Improvements for v0.2.x ------------------------------------------ +## Improvements for v0.3.x - Improve code allowing backward references and preventing forward ones inside a scope -[1] New user-defined validators ------------------------------------------ - +## New user-defined validators +```yaml def: - $MY_VALIDATOR: # name (part of the signature) - [myPath:path, num:integer|boolean, ...rest:string] # arguments (part of the signature) - isType: [{$var: path}, path] # body - $MY_VALIDATOR: [mypath, 3, hello, world] # invocation +``` -[2] R/W variables +## R/W variables and operators ----------------------------------------- -SETTER: -- opSet(var, val) -- opAdd(var, val) -- opSub(var, val) -- opMul(var, val) -- opDiv(var, val) -- opAddMul(var, val, k) -- opSubMul(var, val, k) -- opConcat(var, val) -- opNot(var, val) -- opAnd(var, val) -- opOr(var, val) +- New operators to use in expressions for run-time calculation + - Arithmetic + - @add(val1, ..., valN) + - @sub(val1, ..., valN) + - @mul(val1, ..., valN) + - @div(val1, ..., valN) + - Logical + - @not(val) + - @and(val1, ..., valN) + - @or(val1, ..., valN) + - @xor(val1, ..., valN) + - String + - @concat(val1, ..., valN) + - @substring(str, start, end) + - Conversion + - @toBytes(val) + - @toMillis(val) +- Non constant variables are prefixed by @ and defined as usual + ```yaml + def: + - @var1: hello + @var2: {@concat: [{$var: @var1}, ' world']} + ... + @varN: true + - child + ``` +- New validator assign allows you to set R/W variables already defined and reachable from the current scope + ```yaml + assign: + - @var1: {@add: [3, {@mul: [{$var: @var1.length}, 2]}]} + @var2: {@concat: [{$var: @var2}, '!!!']} + ... + @varN: {@not: [{$var: @varN}]} + - child + ``` +### Example 1: +Consider the object below +```json { - attachments: [ - {content: "1234567890"}, - {content: "qwerty"}, - {content: "zxcvbnm"}, - {content: "a b c d e f g h i"} + "attachments": [ + {"content": "1234567890..."}, + {"content": "qwerty..."}, + {"content": "zxcvbnm..."}, + {"content": "a b c d e f g h i..."} ] } - +``` +You want to check if total content length exceeded a certain value +```yaml def: - - @len: 0 + - @len: 0 # accumulator + - maxLen: {@toBytes: [1 Kb]} # constant - every: - attachments - - and - - add: [@len, {$path: value.content.length}] - - isNumber: [{$var: @len}, { min: 0, max: 100 }] - --------------------------- - + - assign: + - @len: {@add: [{$var: @len}, {$path: value.content.length}]} + - isNumberVar: [{$var: @len}, { min: 0, max: {$var: maxLen} }] +``` + +### Example 2: +Consider the object below +```json { - relatives: [ - {parent: true}, - {parent: false}, - {parent: true}, + "relatives": [ + {"parent": true}, + {"parent": false}, + {"parent": true}, ] } - +``` +You want to check if there are at most two parents amongst relatives +```yaml def: - - numParents: 0 + - @numParents: 0 - every: - relatives - - and: + - if: - equals: [value.parent, true] - - add: [numParents, 1] - - isNumberVar: [{$var: numParents}, { min: 0, max: 2 }] + - assign: + - @numParents: {@add: [{$var: @numParents}, 1] + - isNumberVar: [{$var: @numParents}, { min: 0, max: 2 }] +``` diff --git a/examples/data-driven.js b/examples/data-driven.js index acfbf04..8738ae2 100644 --- a/examples/data-driven.js +++ b/examples/data-driven.js @@ -3,6 +3,7 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); const ensureValidator = require("../lib/ensure-validator"); +const Scope = require("../lib/util/scope"); let toBeValidated = { a: { @@ -17,6 +18,6 @@ let vObj = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "data-driven.yaml" let validator = ensureValidator(vObj); // Validate -let vError = validator(toBeValidated); +let vError = validator(new Scope(toBeValidated)); console.log(`${path.basename(__filename)}: Validation result --> ${vError? vError : "OK!"}`); \ No newline at end of file diff --git a/examples/dsl-validator.js b/examples/dsl-validator.js index b770139..46e9f95 100644 --- a/examples/dsl-validator.js +++ b/examples/dsl-validator.js @@ -3,6 +3,7 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); const ensureValidator = require("../lib/ensure-validator"); +const Scope = require("../lib/util/scope"); // Load DSL validator from file let dslValidator = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "dsl-validator.yaml"), 'utf8')); @@ -10,6 +11,6 @@ let validator = ensureValidator(dslValidator); // Validate the DSL validator itself let toBeValidated = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "dsl-validator.yaml"), 'utf8')); -let vError = validator(toBeValidated); +let vError = validator(new Scope(toBeValidated)); console.log(`${path.basename(__filename)}: Validation result --> ${vError? vError : "OK!"}`); \ No newline at end of file diff --git a/examples/hard-coded.js b/examples/hard-coded.js index 44d6785..2790fec 100644 --- a/examples/hard-coded.js +++ b/examples/hard-coded.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ const path = require("path"); const V = require('../lib'); +const Scope = require("../lib/util/scope"); let toBeValidated = { a: { @@ -24,6 +25,6 @@ let validator = V.and( // Rule 1 // Validate -let vError = validator(toBeValidated); +let vError = validator(new Scope(toBeValidated)); console.log(`${path.basename(__filename)}: Validation result --> ${vError? vError : "OK!"}`); \ No newline at end of file diff --git a/src/branch-validators.js b/src/branch-validators.js index 34cf5eb..88b4fcb 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -12,52 +12,50 @@ function call(path, child) { const infoArgs = call.info.argDescriptors; const pExpr = infoArgs[0].compile(path); const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope, obj); + infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - return cExpr.result(get(obj, pExpr.result), scope); + scope.context.push$(get(scope.find('$'), pExpr.result)); + const result = cExpr.result(scope); + scope.context.pop$(); + return result; }; } function def(resources, child) { const infoArgs = def.info.argDescriptors; - // const sExpr = infoArgs[0].compile(scope, true); // non referenceable object (refDepth = -1) - const childScope = Scope.compile(resources); + const childScope = Scope.compile(undefined, resources); // Parent scope unknown at compile time const cExpr = infoArgs[1].compile(child); - return (obj, scope) => { + return (scope) => { if (!childScope.parent) { childScope.setParent(scope); } if (!childScope.resolved) { // Let's process references - try { - childScope.resolve(obj); - } catch (e) { - return e.message; - } + try { childScope.resolve(); } catch (e) { return e.message; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, childScope, obj); + infoArgs[1].resolve(cExpr, childScope); if (cExpr.error) { return cExpr.error; } } - return cExpr.result(obj, childScope); + return cExpr.result(childScope); }; } function not(child) { const infoArgs = not.info.argDescriptors; const cExpr = infoArgs[0].compile(child); - return (obj, scope = new Scope()) => { + return (scope) => { if (!cExpr.resolved) { - infoArgs[0].resolve(cExpr, scope, obj); + infoArgs[0].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - return cExpr.result(obj, scope) ? undefined : 'not: the child validator must fail'; + return cExpr.result(scope) ? undefined : 'not: the child validator must fail'; }; } @@ -65,17 +63,15 @@ function and(...children) { const { info } = and; const childArg = info.argDescriptors[0]; const offspring = info.compileRestParams(children); - return (obj, scope = new Scope()) => { + return (scope) => { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - childArg.resolve(cExpr, scope, obj); + childArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - const error = (cExpr.result)(obj, scope); // Validate child - if (error) { - return error; - } + const error = cExpr.result(scope); // Validate child + if (error) { return error; } } return undefined; }; @@ -85,18 +81,16 @@ function or(...children) { const { info } = or; const childArg = info.argDescriptors[0]; const offspring = info.compileRestParams(children); - return (obj, scope = new Scope()) => { + return (scope) => { let error; for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - childArg.resolve(cExpr, scope, obj); + childArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - error = (cExpr.result)(obj, scope); // Validate child - if (!error) { - return undefined; - } + error = cExpr.result(scope); // Validate child + if (!error) { return undefined; } } return error; }; @@ -106,19 +100,17 @@ function xor(...children) { const { info } = xor; const childArg = info.argDescriptors[0]; const offspring = info.compileRestParams(children); - return (obj, scope = new Scope()) => { + return (scope) => { let count = 0; for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - childArg.resolve(cExpr, scope, obj); + childArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - const error = (cExpr.result)(obj, scope); // Validate child + const error = cExpr.result(scope); // Validate child count += error ? 0 : 1; - if (count === 2) { - break; - } + if (count === 2) { break; } } return count === 1 ? undefined : `xor: expected exactly 1 valid child; found ${count} instead`; }; @@ -130,24 +122,24 @@ function _if(condChild, thenChild, elseChild) { const ccExpr = infoArgs[0].compile(condChild); const tcExpr = infoArgs[1].compile(thenChild); const ecExpr = infoArgs[2].compile(elseChild); - return (obj, scope = new Scope()) => { + return (scope) => { if (!ccExpr.resolved) { - infoArgs[0].resolve(ccExpr, scope, obj); + infoArgs[0].resolve(ccExpr, scope); if (ccExpr.error) { return ccExpr.error; } } if (!tcExpr.resolved) { - infoArgs[1].resolve(tcExpr, scope, obj); + infoArgs[1].resolve(tcExpr, scope); if (tcExpr.error) { return tcExpr.error; } } if (!ecExpr.resolved) { - infoArgs[2].resolve(ecExpr, scope, obj); + infoArgs[2].resolve(ecExpr, scope); if (ecExpr.error) { return ecExpr.error; } } if (ecExpr.result == null) { - return ccExpr.result(obj, scope) ? undefined : tcExpr.result(obj, scope); + return ccExpr.result(scope) ? undefined : tcExpr.result(scope); } - // either then or else is validated, not both! - return (ccExpr.result(obj, scope) ? ecExpr.result : tcExpr.result)(obj, scope); + // Either then or else is validated, never both! + return (ccExpr.result(scope) ? ecExpr.result : tcExpr.result)(scope); }; } @@ -155,43 +147,56 @@ function every(path, child) { const infoArgs = every.info.argDescriptors; const pExpr = infoArgs[0].compile(path); const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!cExpr.resolved) { infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - const value = get(obj, pExpr.result); + const $ = scope.find('$'); + const value = get($, pExpr.result); if (Array.isArray(value)) { + const new$ = { original: $ }; + scope.context.push$(new$); let error; const found = value.find((item, index) => { - error = cExpr.result({ index, value: item, original: obj }, scope); + new$.value = item; + new$.index = index; + error = cExpr.result(scope); return error; }); + scope.context.pop$(); return found ? error : undefined; } if (typeof value === 'object') { + const new$ = { original: $ }; + scope.context.push$(new$); let error; const found = Object.keys(value).find((key, index) => { - error = cExpr.result({ - index, key, value: value[key], original: obj - }, scope); + new$.key = key; + new$.value = value[key]; + new$.index = index; + error = cExpr.result(scope); return error; }); + scope.context.pop$(); return found ? error : undefined; } if (typeof value === 'string') { + const new$ = { original: $ }; + scope.context.push$(new$); let error; // eslint-disable-next-line no-cond-assign for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { - error = cExpr.result({ index, value: char, original: obj }, scope); - if (error) { - break; - } + new$.value = char; + new$.index = index; + error = cExpr.result(scope); + if (error) { break; } } + scope.context.pop$(); return error; } return `every: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}'`; @@ -202,43 +207,56 @@ function some(path, child) { const infoArgs = some.info.argDescriptors; const pExpr = infoArgs[0].compile(path); const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!cExpr.resolved) { infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - const value = get(obj, pExpr.result); + const $ = scope.find('$'); + const value = get($, pExpr.result); if (Array.isArray(value)) { + const new$ = { original: $ }; + scope.context.push$(new$); let error; const found = value.find((item, index) => { - error = cExpr.result({ index, value: item, original: obj }, scope); + new$.value = item; + new$.index = index; + error = cExpr.result(scope); return !error; }); + scope.context.pop$(); return found ? undefined : error; } if (typeof value === 'object') { + const new$ = { original: $ }; + scope.context.push$(new$); let error; const found = Object.keys(value).find((key, index) => { - error = cExpr.result({ - index, key, value: value[key], original: obj - }, scope); + new$.key = key; + new$.value = value[key]; + new$.index = index; + error = cExpr.result(scope); return !error; }); + scope.context.pop$(); return found ? undefined : error; } if (typeof value === 'string') { + const new$ = { original: $ }; + scope.context.push$(new$); let error; // eslint-disable-next-line no-cond-assign for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { - error = cExpr.result({ index, value: char, original: obj }, scope); - if (!error) { - break; - } + new$.value = char; + new$.index = index; + error = cExpr.result(scope); + if (!error) { break; } } + scope.context.pop$(); return error; } return `some: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}' instead`; @@ -250,20 +268,20 @@ function alter(resultOnSuccess, resultOnError, child) { const sExpr = infoArgs[0].compile(resultOnSuccess); const fExpr = infoArgs[1].compile(resultOnError); const cExpr = infoArgs[2].compile(child); - return (obj, scope = new Scope()) => { + return (scope) => { if (!sExpr.resolved) { - infoArgs[0].resolve(sExpr, scope, obj); + infoArgs[0].resolve(sExpr, scope); if (sExpr.error) { return sExpr.error; } } if (!fExpr.resolved) { - infoArgs[1].resolve(fExpr, scope, obj); + infoArgs[1].resolve(fExpr, scope); if (fExpr.error) { return fExpr.error; } } if (!cExpr.resolved) { - infoArgs[2].resolve(cExpr, scope, obj); + infoArgs[2].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - const r = cExpr.result(obj, scope) === undefined ? sExpr.result : fExpr.result; + const r = cExpr.result(scope) === undefined ? sExpr.result : fExpr.result; return r == null ? undefined : r; }; } @@ -272,16 +290,16 @@ function onError(result, child) { const infoArgs = onError.info.argDescriptors; const rExpr = infoArgs[0].compile(result); const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { + return (scope) => { if (!rExpr.resolved) { - infoArgs[0].resolve(rExpr, scope, obj); + infoArgs[0].resolve(rExpr, scope); if (rExpr.error) { return rExpr.error; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope, obj); + infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - if (cExpr.result(obj, scope) === undefined) { return undefined; } + if (cExpr.result(scope) === undefined) { return undefined; } return rExpr.result == null ? undefined : rExpr.result; }; } @@ -292,63 +310,68 @@ function _while(path, condChild, doChild) { const pExpr = infoArgs[0].compile(path); const ccExpr = infoArgs[1].compile(condChild); const dcExpr = infoArgs[2].compile(doChild); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!ccExpr.resolved) { - infoArgs[1].resolve(ccExpr, scope, obj); + infoArgs[1].resolve(ccExpr, scope); if (ccExpr.error) { return ccExpr.error; } } if (!dcExpr.resolved) { - infoArgs[2].resolve(dcExpr, scope, obj); + infoArgs[2].resolve(dcExpr, scope); if (dcExpr.error) { return dcExpr.error; } } - const value = get(obj, pExpr.result); - const status = { succeeded: 0, failed: 0, original: obj }; + const $ = scope.find('$'); + const value = get($, pExpr.result); + const status = { succeeded: 0, failed: 0, original: $ }; if (Array.isArray(value)) { + scope.context.push$(status); let error; const found = value.find((item, index) => { status.index = index; status.value = item; - error = ccExpr.result(status, scope); + error = ccExpr.result(scope); if (!error) { - status.failed += dcExpr.result(status, scope) ? 1 : 0; + status.failed += dcExpr.result(scope) ? 1 : 0; status.succeeded = index + 1 - status.failed; } return error; }); + scope.context.pop$(); return found ? error : undefined; } if (typeof value === 'object') { + scope.context.push$(status); let error; const found = Object.keys(value).find((key, index) => { status.index = index; status.key = key; status.value = value[key]; - error = ccExpr.result(status, scope); + error = ccExpr.result(scope); if (!error) { - status.failed += dcExpr.result(status, scope) ? 1 : 0; + status.failed += dcExpr.result(scope) ? 1 : 0; status.succeeded = index + 1 - status.failed; } return error; }); + scope.context.pop$(); return found ? error : undefined; } if (typeof value === 'string') { + scope.context.push$(status); let error; // eslint-disable-next-line no-cond-assign for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { status.index = index; status.value = char; - error = ccExpr.result(status, scope); - if (error) { - break; - } - status.failed += dcExpr.result(status, scope) ? 1 : 0; + error = ccExpr.result(scope); + if (error) { break; } + status.failed += dcExpr.result(scope) ? 1 : 0; status.succeeded = index + 1 - status.failed; } + scope.context.pop$(); return error; } return `while: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}'`; diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index e1f847b..39f65f7 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -1,7 +1,6 @@ const v = require('validator'); const { get } = require('../util/path'); const Info = require('../util/info'); -const Scope = require('../util/scope'); class Bridge extends Info { constructor(name, errorFunc, ...noPathArgDescriptors) { @@ -61,17 +60,17 @@ class StringOnly extends Bridge { const pExpr = this.argDescriptors[0].compile(path); const restExpr = this.compileRestParams(noPathArgs, 1); const restValue = []; - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - this.argDescriptors[0].resolve(pExpr, scope, obj); + this.argDescriptors[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } - const errorAt = this.resolveRestParams(restExpr, 1, scope, obj); + const errorAt = this.resolveRestParams(restExpr, 1, scope); if (errorAt >= 0) { return restExpr[errorAt].error; } for (let i = 0, len = restExpr.length; i < len; i += 1) { restValue[i] = restExpr[i].result; } - let value = get(obj, pExpr.result); + let value = get(scope.find('$'), pExpr.result); let result; if (specialized !== undefined && this.isSpecialized(value)) { result = specialized(value, ...restValue); diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 9ffd99f..8d48457 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -4,7 +4,6 @@ const bridge = require('./bridge'); const { get } = require('../util/path'); const createShortcuts = require('../util/create-shortcuts'); const Info = require('../util/info'); -const Scope = require('../util/scope'); const { getType } = require('../util/types'); // @@ -17,22 +16,22 @@ function equals(path, value, deep) { const pExpr = infoArgs[0].compile(path); const vExpr = infoArgs[1].compile(value); const dExpr = infoArgs[2].compile(deep); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!vExpr.resolved) { - infoArgs[1].resolve(vExpr, scope, obj); + infoArgs[1].resolve(vExpr, scope); if (vExpr.error) { return vExpr.error; } } if (!dExpr.resolved) { - infoArgs[2].resolve(dExpr, scope, obj); + infoArgs[2].resolve(dExpr, scope); if (dExpr.error) { return dExpr.error; } } const result = dExpr.result - ? deepEqual(get(obj, pExpr.result), vExpr.result) - : get(obj, pExpr.result) === vExpr.result; + ? deepEqual(get(scope.find('$'), pExpr.result), vExpr.result) + : get(scope.find('$'), pExpr.result) === vExpr.result; return result ? undefined : `equals: the value at path '${path}' must be equal to ${vExpr.result}`; }; } @@ -41,19 +40,19 @@ function isLength(path, options) { const infoArgs = isLength.info.argDescriptors; const pExpr = infoArgs[0].compile(path); const optsExpr = infoArgs[1].compile(options); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!optsExpr.resolved) { - infoArgs[1].resolve(optsExpr, scope, obj); + infoArgs[1].resolve(optsExpr, scope); if (optsExpr.error) { return optsExpr.error; } } const opts = optsExpr.result; const min = opts.min || 0; const max = opts.max; // eslint-disable-line prefer-destructuring - const len = lengthOf(get(obj, pExpr.result)); + const len = lengthOf(get(scope.find('$'), pExpr.result)); if (len === undefined) { return `isLength: the value at path '${path}' must be a string, an array or an object`; } @@ -64,12 +63,12 @@ function isLength(path, options) { function isSet(path) { const infoArgs = isSet.info.argDescriptors; const pExpr = infoArgs[0].compile(path); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } - return get(obj, pExpr.result) != null ? undefined : `isSet: the value at path '${path}' must be set`; + return get(scope.find('$'), pExpr.result) != null ? undefined : `isSet: the value at path '${path}' must be set`; }; } @@ -80,18 +79,18 @@ function isType(path, type) { if (tExpr.resolved) { tExpr.result = getType(tExpr.result); } - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!tExpr.resolved) { - infoArgs[1].resolve(tExpr, scope, obj); + infoArgs[1].resolve(tExpr, scope); if (tExpr.error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } const t = tExpr.result; - return t.check(get(obj, pExpr.result)) ? undefined : `isType: the value at path '${path}' must be a '${t.name}'`; + return t.check(get(scope.find('$'), pExpr.result)) ? undefined : `isType: the value at path '${path}' must be a '${t.name}'`; }; } @@ -99,16 +98,16 @@ function isOneOf(path, values) { const infoArgs = isOneOf.info.argDescriptors; const pExpr = infoArgs[0].compile(path); const aExpr = infoArgs[1].compile(values); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!aExpr.resolved) { - infoArgs[1].resolve(aExpr, scope, obj); + infoArgs[1].resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } - return aExpr.result.includes(get(obj, pExpr.result)) ? undefined : `isOneOf: the value at path '${path}' must be one of ${aExpr.result}`; + return aExpr.result.includes(get(scope.find('$'), pExpr.result)) ? undefined : `isOneOf: the value at path '${path}' must be one of ${aExpr.result}`; }; } @@ -119,17 +118,17 @@ function isArrayOf(path, type) { if (tExpr.resolved) { tExpr.result = getType(tExpr.result); } - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); + infoArgs[0].resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } if (!tExpr.resolved) { - infoArgs[1].resolve(tExpr, scope, obj); + infoArgs[1].resolve(tExpr, scope); if (tExpr.error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } - const value = get(obj, pExpr.result); + const value = get(scope.find('$'), pExpr.result); const t = tExpr.result; if (!Array.isArray(value)) return `isArrayOf: the value at path '${path}' must be an array`; const flag = value.every(e => t.check(e)); diff --git a/src/util/argument.js b/src/util/argument.js index b73a43e..1db5d57 100644 --- a/src/util/argument.js +++ b/src/util/argument.js @@ -38,8 +38,8 @@ class Argument { return this.type.compile(value, noReference || this.refDepth < 0); } - resolve(expr, scope, obj) { - return this.type.resolve(expr, scope, obj); + resolve(expr, scope) { + return this.type.resolve(expr, scope); } } diff --git a/src/util/context.js b/src/util/context.js index a9c458b..eb31958 100644 --- a/src/util/context.js +++ b/src/util/context.js @@ -1,11 +1,16 @@ const { NATIVE_TYPES, UnionType } = require('./types'); class Context { - constructor() { + constructor($) { + this.stack$ = [$]; this.types = {}; } static find(scope, name, defaultValue) { + if (name === '$') { + const { stack$ } = scope.context; + return stack$[stack$.length - 1]; + } for (let curScope = scope; curScope != null; curScope = curScope.parent) { if (name in curScope.resources) { return curScope.resources[name]; // Found in current scope @@ -14,6 +19,14 @@ class Context { return defaultValue; // Not found in any scope } + push$(obj) { + this.stack$.push(obj); + } + + pop$() { + this.stack$.length = Math.max(0, this.stack$.length - 1); + } + getType(name) { // Search type name in native types if (name in NATIVE_TYPES) { diff --git a/src/util/create-shortcuts.js b/src/util/create-shortcuts.js index 9e0321b..8ea950a 100644 --- a/src/util/create-shortcuts.js +++ b/src/util/create-shortcuts.js @@ -2,7 +2,6 @@ const camelCase = require('camelcase'); const { get } = require('./path'); const Info = require('./info'); const Argument = require('./argument'); -const Scope = require('./scope'); function getFirstArgType(validator) { const ads = validator.info.argDescriptors; @@ -14,12 +13,12 @@ function optShortcutOf(validator, name) { const optV = (path, ...args) => { const argDescriptor0 = info.argDescriptors[0]; const pExpr = argDescriptor0.compile(path); - return (obj, scope = new Scope()) => { + return (scope) => { if (!pExpr.resolved) { - argDescriptor0.resolve(pExpr, scope, obj); + argDescriptor0.resolve(pExpr, scope); if (pExpr.error) { return pExpr.error; } } - return (get(obj, pExpr.result) ? validator(pExpr.result, ...args)(obj, scope) : undefined); + return (get(scope.find('$'), pExpr.result) ? validator(pExpr.result, ...args)(scope) : undefined); }; }; Object.defineProperty(optV, 'name', { value: name, writable: false }); diff --git a/src/util/expression.js b/src/util/expression.js index 7029ffd..cb0b6ac 100644 --- a/src/util/expression.js +++ b/src/util/expression.js @@ -1,10 +1,8 @@ const camelCase = require('camelcase'); -const clone = require('rfdc')({ proto: false, circles: false }); const { get, set, ensureArrayPath } = require('./path'); -const { checkUniqueKey, ANY_VALUE } = require('./misc'); +const { checkUniqueKey, ANY_VALUE, clone } = require('./misc'); const REF_VALID_KEYS = { - $path: true, $var: true }; @@ -14,18 +12,12 @@ function unexpectedReferenceError(key, value, refType, type) { function createRefPath(type, tPath, key, value) { const targetPath = ensureArrayPath(tPath); - if (key === '$path') { - if (type && type.acceptsValidator && !type.acceptsValue) { - throw unexpectedReferenceError(key, value, 'value', type.name); - } - return { targetPath, path: ensureArrayPath(value) }; // no varName - } // Split value in varName and path const index = value.indexOf('.'); const varName = index < 0 ? value : value.substr(0, index); const path = index < 0 ? '' : value.substr(index + 1); if (type) { - const isValidator = varName.startsWith('$'); + const isValidator = varName.startsWith('$') && varName.length > 1; if (isValidator) { if (!type.acceptsValidator) { throw unexpectedReferenceError(key, value, 'validator', type.name); @@ -72,14 +64,10 @@ function prepareRefPaths(type, o, refPaths, path) { const VAR_NOT_FOUND = {}; // Any error is reported to the reference by setting its error property -function resolveRefPathAt(reference, index, scope, obj) { +function resolveRefPathAt(reference, index, scope) { const rp = reference.refPaths[index]; - if (rp.varName === undefined) { - // Return the value at the referenced path in the input object - return get(obj, rp.path); - } // Retrieve the referenced variable/validator - const isValidator = rp.varName.startsWith('$'); + const isValidator = rp.varName.startsWith('$') && rp.varName.length > 1; const value = scope.find(rp.varName, VAR_NOT_FOUND); if (value === VAR_NOT_FOUND) { reference.setError(`Unresolved ${isValidator ? 'validator' : 'value'} reference to '${rp.varName}'`); @@ -115,19 +103,19 @@ class Expression { return prepareRefPaths(type, source); } - resolve(scope, obj) { + resolve(scope) { if (this.error || this.resolved) { return this; } if (this.isRootRef) { // Resolve root reference - this.result = resolveRefPathAt(this, 0, scope, obj); + this.result = resolveRefPathAt(this, 0, scope); } else { // Resolve embedded references const { result: value, refPaths } = this; for (let i = 0, len = refPaths.length; i < len; i += 1) { - const rpValue = resolveRefPathAt(this, i, scope, obj); + const rpValue = resolveRefPathAt(this, i, scope); if (this.error) { break; } diff --git a/src/util/info.js b/src/util/info.js index 1d892ed..ab34bcc 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -27,11 +27,11 @@ class Info { } // Returns the index of the first param where an error occurred; -1 otherwise - resolveRestParams(exprs, offset, scope, obj) { + resolveRestParams(exprs, offset, scope) { for (let i = 0, len = exprs.length; i < len; i += 1) { if (!exprs[i].resolved) { const ad = this.argDescriptors[this.adjustArgDescriptorIndex(i + offset)]; - ad.resolve(exprs[i], scope, obj); + ad.resolve(exprs[i], scope); if (exprs[i].error) { return i; } diff --git a/src/util/misc.js b/src/util/misc.js index fc992aa..78f7610 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,3 +1,5 @@ +const clone = require('rfdc')({ proto: false, circles: false }); + const ANY_VALUE = Object.freeze(['boolean', 'number', 'object', 'string', 'undefined'] .reduce((acc, k) => { acc[k] = true; @@ -33,5 +35,6 @@ function lazyProperty(instance, key, value, writable, configurable) { module.exports = { ANY_VALUE, checkUniqueKey, + clone, lazyProperty }; diff --git a/src/util/scope.js b/src/util/scope.js index ce136e5..9a18d7c 100644 --- a/src/util/scope.js +++ b/src/util/scope.js @@ -9,23 +9,32 @@ const any = getNativeType('any'); const child = getNativeType('child'); class Scope { - constructor(parent, resources) { - this.parent = parent; - this.context = parent ? parent.context : new Context(); + constructor(parentOrContextOr$, resources) { + if (parentOrContextOr$ instanceof Scope) { + // Non root scope inherits context from parent + this.parent = parentOrContextOr$; + this.context = this.parent.context; + } else { + // Root scope needs either an explicit context or the object to validate + this.parent = undefined; + this.context = parentOrContextOr$ instanceof Context + ? parentOrContextOr$ + : new Context(parentOrContextOr$); + } this.resources = resources || {}; this.resolved = false; } setParent(parent) { this.parent = parent; - this.context = parent ? parent.context : new Context(); + this.context = parent ? parent.context : undefined; } find(name, defaultValue) { return Context.find(this, name, defaultValue); } - static compile(resources) { + static compile(parentOrContextOr$, resources) { if (!isPlainObject(resources)) { throw new Error('Expected a scope of type \'object\''); } @@ -38,7 +47,7 @@ class Scope { if (hasOwn.call(resources, k)) { const cur = resources[k]; if (typeof cur === 'object' && cur !== null) { - const type = k.startsWith('$') ? child : any; + const type = k.startsWith('$') && k.length > 1 ? child : any; const ref = type.compile(cur); // The check (ref.result !== cur) detects both // references and non hard-coded validators @@ -52,12 +61,12 @@ class Scope { } } } - const scope = new Scope(null, target); + const scope = new Scope(parentOrContextOr$, target); scope.resolved = target === resources; return scope; } - resolve(obj) { + resolve() { if (!this.resolved) { const compiledResources = this.resources; // Set resources to a fresh new object in order to add properties progressively @@ -68,8 +77,8 @@ class Scope { if (hasOwn.call(compiledResources, k)) { let resource = compiledResources[k]; if (resource instanceof Expression) { - const type = k.startsWith('$') ? child : any; - const ref = type.resolve(resource, this, obj); + const type = k.startsWith('$') && k.length > 1 ? child : any; + const ref = type.resolve(resource, this); if (ref.error) { throw new Error(ref.error); } resource = ref.result; } @@ -77,7 +86,7 @@ class Scope { // to override the invocation scope with its definition scope, where // the validator must run this.resources[k] = typeof resource === 'function' - ? object => resource(object, this) + ? () => resource(this) : resource; } } diff --git a/src/util/types.js b/src/util/types.js index c8fa2c3..ccab75f 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -71,8 +71,8 @@ class Type { return expr; } - resolve(expr, scope, obj) { - return this.checkExpr(expr.resolve(scope, obj)); + resolve(expr, scope) { + return this.checkExpr(expr.resolve(scope)); } } @@ -200,11 +200,6 @@ class ChildType extends Type { expr.result = validate(...val[method]); // eslint-disable-line no-param-reassign return expr; } - // if (method === '$path') { - // // Doesn't make sense taking a validator from the object to validate. - // // It sounds like an error. So let's prevent this from occurring. - // return expr.setError(`Unexpected reference '${JSON.stringify(val)}' for a validator`); - // } return expr.setError(`Error: Unknown validator '${method}'`); } return expr.setError(`Expected a validator as either a function or a plain object; found a ${typeof val} instead`); @@ -258,7 +253,7 @@ class UnionType extends Type { const tn = m[i]; const qmIndex = tn.lastIndexOf('?'); if (qmIndex >= 0) { - if (m === members) { // clone input array before changing any item + if (m === members) { // Make a shallow copy of input array before changing any item m = Array.from(m); } m[i] = tn.substring(0, qmIndex); diff --git a/test/branch-validators/def.js b/test/branch-validators/def.js index 92cb754..6e6e055 100644 --- a/test/branch-validators/def.js +++ b/test/branch-validators/def.js @@ -1,5 +1,6 @@ import { assert } from 'chai'; import V from '../../src'; +import Scope from '../../src/util/scope'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; @@ -16,15 +17,15 @@ describe('Test branch validator def.', () => { testValidation(FAILURE, { a: 'not -3.14' }, V.def, { v1: -3.14 }, { equals: ['a', { $var: 'v1' }] }); it('def({}, V.optIsSet("")) should always succeed just like its child', () => { const v = V.def({}, V.optIsSet('')); - assert(v({ a: 123 }) === undefined, ':('); + assert(v(new Scope({ a: 123 })) === undefined, ':('); }); it('def({$TEST: V.optIsSet("")}, {$var: "$TEST"}) should always succeed just like its referenced hard-coded child', () => { const v = V.def({ $TEST: V.optIsSet('') }, { $var: '$TEST' }); - assert(v({ a: 123 }) === undefined, ':('); + assert(v(new Scope({ a: 123 })) === undefined, ':('); }); it('def({$TEST: {optIsSet: [""]}, {$var: "$TEST"}) should always succeed just like its referenced soft-coded child', () => { const v = V.def({ $TEST: { optIsSet: [''] } }, { $var: '$TEST' }); - assert(v({ a: 123 }) === undefined, ':('); + assert(v(new Scope({ a: 123 })) === undefined, ':('); }); it('Scope of inner def can reference variable of outer def', () => { const v = V.def( @@ -34,7 +35,7 @@ describe('Test branch validator def.', () => { { equals: ['a', { $var: 'v2' }] } ) ); - assert(v({ a: 123 }) === undefined, ':('); + assert(v(new Scope({ a: 123 })) === undefined, ':('); }); it('Variable in a scope can make a backward reference to a variable in the same scope', () => { const v = V.def( @@ -44,7 +45,7 @@ describe('Test branch validator def.', () => { }, { equals: ['a', { $var: 'v2' }] } ); - assert(v({ a: 123 }) === undefined, ':('); + assert(v(new Scope({ a: 123 })) === undefined, ':('); }); it('Variable in a scope cannot make a forward reference to a variable in the same scope', () => { const v = V.def( @@ -54,7 +55,7 @@ describe('Test branch validator def.', () => { }, { equals: ['a', { $var: 'v2' }] } ); - assert(v({ a: 123 }) !== undefined, ':('); + assert(v(new Scope({ a: 123 })) !== undefined, ':('); }); it('Validator should run in the scope of its definition, not the scope of its invocation', () => { const v = V.def( @@ -67,6 +68,6 @@ describe('Test branch validator def.', () => { { $var: '$validator1' } ) ); - assert(v({ a: 123 }) === undefined, ':('); + assert(v(new Scope({ a: 123 })) === undefined, ':('); }); }); diff --git a/test/branch-validators/if.js b/test/branch-validators/if.js index aac6ddf..b43a5ba 100644 --- a/test/branch-validators/if.js +++ b/test/branch-validators/if.js @@ -1,5 +1,6 @@ import { assert } from 'chai'; import V from '../../src'; +import Scope from '../../src/util/scope'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { FAILURE } = VALIDATION; @@ -14,25 +15,25 @@ describe('Test branch validator if.', () => { testValidation(FAILURE, {}, V.if, success, vThen, vElse); it('if(success, then, else) should return then validation result', () => { const v = V.if(success, vThen, vElse); - assert(v({}) === 'then', ':('); + assert(v(new Scope({})) === 'then', ':('); }); it('if(failure, then, else) should return else validation result', () => { const v = V.if(failure, vThen, vElse); - assert(v({}) === 'else', ':('); + assert(v(new Scope({})) === 'else', ':('); }); it('if(success, then) should return then validation result', () => { const v = V.if(success, vThen); - assert(v({}) === 'then', ':('); + assert(v(new Scope({})) === 'then', ':('); }); it('if(failure, then) should be always valid', () => { const v = V.if(failure, vThen); - assert(v({}) === undefined, ':('); + assert(v(new Scope({})) === undefined, ':('); }); const notBoth = (condStr, condFunc) => it(`if(${condStr}, then, else) should validate either then or else, never both`, () => { let count = 0; const inc = () => { count += 1; }; const v = V.if(condFunc, inc, inc); - v({}); + v(new Scope({})); assert(count === 1, ':('); }); notBoth('success', success); diff --git a/test/branch-validators/iterators.js b/test/branch-validators/iterators.js index 0d60c45..4ab8b22 100644 --- a/test/branch-validators/iterators.js +++ b/test/branch-validators/iterators.js @@ -1,6 +1,7 @@ import { assert } from 'chai'; import lengthOf from '@davebaol/length-of'; import V from '../../src'; +import Scope from '../../src/util/scope'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; @@ -29,7 +30,7 @@ function testEveryOrSome(name) { return count === expected ? failureForEvery() : successForEvery(); }; const v = everyOrSome(t, vIt); - v(test); + v(new Scope(test)); assert(count === expected, ':('); })); testKeys.forEach(t => it(`For ${t}s ${name} should ${isEvery ? 'succeed when all iterations are valid' : 'fail when all iterations are invalid'}`, () => { @@ -37,15 +38,15 @@ function testEveryOrSome(name) { const expected = lengthOf(test[t]); const vIt = () => { count += 1; return successForEvery(); }; const v = everyOrSome(t, vIt); - v(test); + v(new Scope(test)); assert(count === expected, ':('); })); function iterationChecker(type, expected) { it(`For ${type}s ${name} should generate proper iteration objects`, () => { const actual = []; - const vIt = (m) => { actual.push(m); return successForEvery(); }; + const vIt = (scope) => { actual.push(Object.assign({}, scope.find('$'))); return successForEvery(); }; const v = everyOrSome(type, vIt); - v(test); + v(new Scope(test)); assert.deepEqual(actual, expected, ':('); }); } @@ -59,7 +60,7 @@ function testEveryOrSome(name) { Object.keys(failureExpected).forEach(k => it(`For ${k} ${name} should fail`, () => { const vIt = () => undefined; const v = everyOrSome('', vIt); - assert(v(failureExpected[k]) !== undefined, ':('); + assert(v(new Scope(failureExpected[k])) !== undefined, ':('); })); }); } @@ -90,9 +91,9 @@ describe('Test branch validator while.', () => { it(`For ${type}s while should generate proper iteration objects`, () => { const actual = []; const vCond = V.optIsSet(''); // always true - const vDo = (obj) => { actual.push(Object.assign({}, obj)); return undefined; }; + const vDo = (scope) => { actual.push(Object.assign({}, scope.find('$'))); return undefined; }; const v = V.while(type, vCond, vDo); - v(test); + v(new Scope(test)); assert.deepEqual(actual, expected, ':('); }); } @@ -109,7 +110,7 @@ describe('Test branch validator while.', () => { const failureExpected = { numbers: 123, booleans: true }; Object.keys(failureExpected).forEach(k => it(`For ${k} while should fail`, () => { const v = V.while('', () => undefined, () => undefined); - assert(v(failureExpected[k]) !== undefined, ':('); + assert(v(new Scope(failureExpected[k])) !== undefined, ':('); })); function checkParents(shouldSucceed) { @@ -128,7 +129,8 @@ describe('Test branch validator while.', () => { V.isInt('succeeded', { min: 0, max: 2 }), V.equals('value.parent', true) ); - assert(shouldSucceed ? (v(person) === undefined) : (v(person) !== undefined), ':('); + const result = v(new Scope(person)); + assert(shouldSucceed ? result === undefined : result !== undefined, ':('); }); } checkParents(true); diff --git a/test/branch-validators/misc.js b/test/branch-validators/misc.js index 20e0b04..ae6b50e 100644 --- a/test/branch-validators/misc.js +++ b/test/branch-validators/misc.js @@ -1,5 +1,6 @@ import { assert } from 'chai'; import V from '../../src'; +import Scope from '../../src/util/scope'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS } = VALIDATION; @@ -13,11 +14,11 @@ describe('Test branch validator alter.', () => { testValidation(SUCCESS, {}, V.alter, 'error', null, failure); it('alter(success, "OK", "KO") should return "OK"', () => { const v = V.alter('OK', 'KO', success); - assert(v({}) === 'OK', ':('); + assert(v(new Scope({})) === 'OK', ':('); }); it('alter(failure, "OK", "KO") should return "KO"', () => { const v = V.alter('OK', 'KO', failure); - assert(v({}) === 'KO', ':('); + assert(v(new Scope({})) === 'KO', ':('); }); }); @@ -26,10 +27,10 @@ describe('Test branch validator onError.', () => { testValidation(SUCCESS, {}, V.onError, null, failure); it('onError("success, error") should succeed', () => { const v = V.onError('error', success); - assert(v({}) === undefined, ':('); + assert(v(new Scope({})) === undefined, ':('); }); it('onError(failure, "error") should fail with "error"', () => { const v = V.onError('error', failure); - assert(v({}) === 'error', ':('); + assert(v(new Scope({})) === 'error', ':('); }); }); diff --git a/test/leaf-validators/equals.js b/test/leaf-validators/equals.js index 8c453ca..42912dc 100644 --- a/test/leaf-validators/equals.js +++ b/test/leaf-validators/equals.js @@ -1,6 +1,7 @@ import { assert } from 'chai'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; import V from '../../src'; +import Scope from '../../src/util/scope'; const { SUCCESS, FAILURE } = VALIDATION; @@ -17,7 +18,8 @@ describe('Test leaf validator equals.', () => { const obj1 = { a: 'hello', b: [undefined, { x: true }] }; const obj2 = { a: 'hello', b: [b, { x: true }] }; const v = V.equals('', obj1, true); - assert(b === null ? v(obj2) !== undefined : v(obj2) === undefined, ':('); + const result = v(new Scope(obj2)); + assert(b === null ? result !== undefined : result === undefined, ':('); })); // Deep equal with reference @@ -25,6 +27,6 @@ describe('Test leaf validator equals.', () => { const obj1 = { a: 'hello' }; const obj2 = { a: 'hello' }; const v = V.def({ deep: true }, V.equals('', obj1, { $var: 'deep' })); - assert(v(obj2) === undefined, ':('); + assert(v(new Scope(obj2)) === undefined, ':('); }); }); diff --git a/test/leaf-validators/isLength.js b/test/leaf-validators/isLength.js index 455c291..e93db46 100644 --- a/test/leaf-validators/isLength.js +++ b/test/leaf-validators/isLength.js @@ -1,5 +1,6 @@ import { assert } from 'chai'; import V from '../../src'; +import Scope from '../../src/util/scope'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; @@ -14,11 +15,11 @@ function check(val, shouldSucceed) { function checkRef(val, shouldSucceed) { const obj = { a: val, referenced: 3 }; - const options = { min: { $path: 'referenced' }, max: 3 }; + const options = { min: { $var: '$.referenced' }, max: 3 }; it(`isLength("a", ${JSON.stringify(options)}) should ${shouldSucceed ? 'succeed' : 'fail'} for ${JSON.stringify(obj)}`, () => { const v = V.isLength('a', options); - const result = shouldSucceed ? v(obj) === undefined : v(obj) !== undefined; - assert(result, ':('); + const result = v(new Scope(obj)); + assert(shouldSucceed ? result === undefined : result !== undefined, ':('); }); } diff --git a/test/test-utils.js b/test/test-utils.js index cb594d3..45c9357 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,5 +1,7 @@ import { assert } from 'chai'; import Expression from '../src/util/expression'; +import Scope from '../src/util/scope'; +import { clone } from '../src/util/misc'; import V from '../src'; const UNKNOWN_REF = Object.freeze({ $unknownRefType: 'anything' }); @@ -70,10 +72,6 @@ const typeInfo = Object.keys(V).reduce((acc, k) => { return acc; }, {}); -function clone(data) { - return JSON.parse(JSON.stringify(data)); -} - function ordinal(n) { switch (n) { case 1: return '1st'; @@ -123,19 +121,9 @@ function testArgument(vld, args, index, errorLike) { assert.throws(() => vld(...testArgs), errorLike || Error); }); } - ['$path', '$var'].forEach((refType) => { - if (refType === '$path' && argDesc.type.acceptsValidator && !argDesc.type.acceptsValue) { - // it(`Should throw immediately an error on $path reference - // as ${ordinal(index + 1)} argument`, () => { - // testArgs[index] = { [refType]: 'any.path' }; - // assert.throws(() => vld(...testArgs), Error, 'Unexpected reference \'{"$path":'); - // }); - } else { - it(`Should delay ${refType} reference resolution at validation time for ${kind} as ${ordinal(index + 1)} argument`, () => { - testArgs[index] = { [refType]: `${argDesc.type.acceptsValidator ? '$someValidator' : 'some.value'}` }; - assert(typeof vld(...testArgs) === 'function', ':('); - }); - } + it(`Should delay $var reference resolution at validation time for ${kind} as ${ordinal(index + 1)} argument`, () => { + testArgs[index] = { $var: `${argDesc.type.acceptsValidator ? '$someValidator' : 'some.value'}` }; + assert(typeof vld(...testArgs) === 'function', ':('); }); } } @@ -150,16 +138,16 @@ const VALIDATION = Object.freeze({ function testValidationAssert(expectedResult, vCreate, obj) { switch (expectedResult) { case VALIDATION.SUCCESS: - assert(vCreate()(obj) === undefined, ':('); + assert(vCreate()(new Scope(obj)) === undefined, ':('); break; case VALIDATION.FAILURE: - assert(vCreate()(obj) !== undefined, ':('); + assert(vCreate()(new Scope(obj)) !== undefined, ':('); break; case VALIDATION.THROW: - assert.throws(() => vCreate()(obj), Error); + assert.throws(() => vCreate()(new Scope(obj)), Error); break; case VALIDATION.DO_NOT_THROW: - assert.doesNotThrow(() => vCreate()(obj), Error); + assert.doesNotThrow(() => vCreate()(new Scope(obj)), Error); break; default: assert(false, 'Unknown expected result'); @@ -183,12 +171,15 @@ function testValidationWithNoRefs(expected, obj, vld, ...args) { } // All referenceable arguments are passed as $var references -function testValidationWithVarRefs(expected, obj, vld, ...args) { +// If bad is >= 0 then the arg at that index is passed +// as a reference to a bad value. +function testValidationWithVarRefs0(expected, obj, vld, args, bad = -1) { const scope = args.reduce((acc, a, i) => { const argDef = vld.info.argDescriptors[vld.info.adjustArgDescriptorIndex(i)]; if (argDef.refDepth >= 0) { const kind = argDef.type.name; - acc[`${typeInfo[kind].acceptValidatorRef() ? '$' : ''}v${i}`] = a; + const value = i !== bad ? a : typeInfo[kind].badValue; + acc[`${typeInfo[kind].acceptValidatorRef() ? '$' : ''}v${i}`] = value; } return acc; }, {}); @@ -206,30 +197,16 @@ function testValidationWithVarRefs(expected, obj, vld, ...args) { }); } -// All referenceable arguments are passed as $path references -function testValidationWithPathRefs(expected, obj, vld, ...args) { - const obj2 = args.reduce((acc, a, i) => { - const argDef = vld.info.argDescriptors[vld.info.adjustArgDescriptorIndex(i)]; - if (argDef.refDepth >= 0) { - const kind = argDef.type.name; - if (!typeInfo[kind].acceptValidatorRef()) { - acc[`_${i}`] = a; - } - } - return acc; - }, clone(obj)); - const pathArgs = args.map((a, i) => { +function testValidationWithVarRefs(expected, obj, vld, ...args) { + // All references to good value + testValidationWithVarRefs0(expected, obj, vld, args); + + // Use reference to a bad value for one argument at a time + for (let i = 0; i < args.length; i += 1) { const argDef = vld.info.argDescriptors[vld.info.adjustArgDescriptorIndex(i)]; - const kind = argDef.type.name; - if (argDef.refDepth >= 0) { - return typeInfo[kind].acceptValidatorRef() ? a : { $path: `_${i}` }; - } - return a; - }); - it(`${vld.info.name}(${pathArgs.map(a => JSON.stringify(a)).join(', ')}) should ${expected} for ${JSON.stringify(obj2)}`, () => { - const vCreate = () => vld(...pathArgs); - testValidationAssert(expected, vCreate, obj2); - }); + const expected2 = argDef.type.acceptsValidator ? VALIDATION.THROW : VALIDATION.FAILURE; + testValidationWithVarRefs0(expected2, obj, vld, clone(args), i); + } } function testValidation(expectedResult, obj, vld, ...args) { @@ -244,14 +221,10 @@ function testValidation(expectedResult, obj, vld, ...args) { // Test $var references testValidationWithVarRefs(expected[1], obj, vld, ...args); - - // Test $path references - testValidationWithPathRefs(expected[2], obj, vld, ...args); } export { typeInfo as argInfo, - clone, testArgument, testAllArguments, testValidation, diff --git a/test/util/context.js b/test/util/context.js index 6f610e8..52fde02 100644 --- a/test/util/context.js +++ b/test/util/context.js @@ -6,7 +6,7 @@ function typesToArray(typesAsString) { } function typesTypeInstance(typesAsString, useArray) { - const context = new Context(); + const context = new Context({}); const get1 = context.getType(typesAsString); const get2 = context.getType(useArray ? typesToArray(typesAsString) : typesAsString); assert(get1 === get2, ':('); diff --git a/test/util/create-shortcuts.js b/test/util/create-shortcuts.js index 9389256..625cdf8 100644 --- a/test/util/create-shortcuts.js +++ b/test/util/create-shortcuts.js @@ -1,6 +1,7 @@ import { assert } from 'chai'; import camelcase from 'camelcase'; import V from '../../src'; +import Scope from '../../src/util/scope'; import createShortcuts from '../../src/util/create-shortcuts'; describe('Test shortcut opt.', () => { @@ -26,23 +27,23 @@ describe('Test shortcut opt.', () => { it('Missing property at path should always succeed', () => { const target = createShortcuts({}, V, ['isSet']); const v = target.optIsSet('a'); - assert(v({}) === undefined, ':('); + assert(v(new Scope({})) === undefined, ':('); }); it('Not missing property at path should always match the original validator', () => { const target = createShortcuts({}, V, ['isSet']); const v1 = target.optIsSet('a'); const v2 = V.isSet('a'); - assert(v1({ a: 0 }) === v2({ a: 32 }), ':('); + assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); }); it('Missing property at referenced path should always succeed', () => { const target = createShortcuts({}, V, ['isSet']); const v = V.def({ p: 'a' }, target.optIsSet({ $var: 'p' })); - assert(v({}) === undefined, ':('); + assert(v(new Scope({})) === undefined, ':('); }); it('Not missing property at referenced path should always match the original validator', () => { const target = createShortcuts({}, V, ['isSet']); const v1 = V.def({ p: 'a' }, target.optIsSet({ $var: 'p' })); const v2 = V.def({ p: 'a' }, V.optIsSet({ $var: 'p' })); - assert(v1({ a: 0 }) === v2({ a: 32 }), ':('); + assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); }); }); diff --git a/test/util/expression.js b/test/util/expression.js index ae16d19..8b254d9 100644 --- a/test/util/expression.js +++ b/test/util/expression.js @@ -40,12 +40,12 @@ function testEmbeddedRefCreate(type, source, expectedRefPaths) { function tesResolve(type, isRoot, source, resources, obj, expected) { it(`Resolving ${isRoot ? 'root' : 'embedded'} reference ${JSON.stringify(source)} in scope ${JSON.stringify(resources)} on object ${JSON.stringify(obj)} should return ${JSON.stringify(expected)}.`, () => { - const scope = Scope.compile(resources); + const scope = Scope.compile(obj, resources); if (!scope.resolved) { throw new Error('Fix your test!!! Scope with references are not supported by this test'); } const expr = new Expression(type, source); - assert.deepEqual(expr.resolve(scope, obj).result, expected, ':('); + assert.deepEqual(expr.resolve(scope).result, expected, ':('); }); } @@ -63,19 +63,20 @@ describe('Test Expression class.', () => { testRefCreateForValidatorWithDeepPath(childType); testRootRefCreate(childType, { $var: '$myValidator' }, [refPath('', '$myValidator', '')]); testRootRefCreate(integerType, { $var: 'record.fields.1' }, [refPath('', 'record', 'fields.1')]); - testRootRefCreate(integerType, { $path: 'record.fields.1' }, [refPath('', undefined, 'record.fields.1')]); - testEmbeddedRefCreate(integerType, { - bounds: { - upper: { $var: 'record.fields.1' }, - lower: { $path: 'a.b.0' } + testRootRefCreate(integerType, { $var: '$.record.fields.1' }, [refPath('', '$', 'record.fields.1')]); + testEmbeddedRefCreate(integerType, + { + bounds: { + upper: { $var: 'record.fields.1' }, + lower: { $var: '$.a.b.0' } + }, + dummy: true }, - dummy: true - }, - [ - refPath('bounds.upper', 'record', 'fields.1'), - refPath('bounds.lower', undefined, 'a.b.0') - ]); + [ + refPath('bounds.upper', 'record', 'fields.1'), + refPath('bounds.lower', '$', 'a.b.0') + ]); tesResolve(integerType, true, { $var: 'record.fields.1' }, { record: { fields: ['xyz', 123, true] } }, {}, 123); - tesResolve(integerType, true, { $path: 'record.fields.1' }, {}, { record: { fields: ['xyz', 123, true] } }, 123); - tesResolve(integerType, false, [{ $var: 'record.0' }, { $path: 'a' }], { record: ['hello', 'world'] }, { a: 123 }, ['hello', 123]); + tesResolve(integerType, true, { $var: '$.record.fields.1' }, {}, { record: { fields: ['xyz', 123, true] } }, 123); + tesResolve(integerType, false, [{ $var: 'record.0' }, { $var: '$.a' }], { record: ['hello', 'world'] }, { a: 123 }, ['hello', 123]); }); diff --git a/test/util/scope.js b/test/util/scope.js index f21fcfc..fad535a 100644 --- a/test/util/scope.js +++ b/test/util/scope.js @@ -3,26 +3,26 @@ import Scope from '../../src/util/scope'; import Expression from '../../src/util/expression'; import V from '../../src'; -describe('Test utility Scope.compile(scope).', () => { +describe('Test utility Scope.compile($, scope).', () => { it('Should throw an error if the scope is a root reference', () => { const defs = { $var: 'MY_OTHER_SCOPE' }; - assert.throws(() => Scope.compile(defs), Error, 'Root reference not allowed'); + assert.throws(() => Scope.compile(null, defs), Error, 'Root reference not allowed'); }); it('Should return the same scope specified in input, if all its variables have no references (constants)', () => { const defs = { VARIABLE: 123 }; - assert(Scope.compile(defs).resources === defs, ':('); + assert(Scope.compile(null, defs).resources === defs, ':('); }); it('Should return the same scope specified in input, if all its its validators are hard-coded', () => { const defs = { $VALIDATOR: V.contains('a', 'x') }; - assert(Scope.compile(defs).resources === defs, ':('); + assert(Scope.compile(null, defs).resources === defs, ':('); }); it('Should return a new scope, if the scope specified in input defines any variable with references (non constant)', () => { const defs = { VARIABLE: { $var: 'V1' } }; - assert(Scope.compile(defs).resources !== defs, ':('); + assert(Scope.compile(null, defs).resources !== defs, ':('); }); it('Should return a new scope, if the scope specified in input defines any validator in the form of data', () => { const defs = { $VALIDATOR: { contains: ['a', 'x'] } }; - assert(Scope.compile(defs).resources !== defs, ':('); + assert(Scope.compile(null, defs).resources !== defs, ':('); }); it('Should return a new scope where the variables and validators have the expected type', () => { const defs = { @@ -32,7 +32,7 @@ describe('Test utility Scope.compile(scope).', () => { $VALIDATOR_2: { contains: ['a', 'x'] }, // validator in the form of data $VALIDATOR_3: { $var: '$VALIDATOR_1' } // validator reference }; - const { resources } = Scope.compile(defs); + const { resources } = Scope.compile(null, defs); const expected = resources !== defs && typeof resources.VAR_1 === 'object' && !(resources.VAR_1 instanceof Expression) && resources.VAR_2 instanceof Expression diff --git a/test/util/types/ref.js b/test/util/types/ref.js index f565c3c..c5df214 100644 --- a/test/util/types/ref.js +++ b/test/util/types/ref.js @@ -15,18 +15,13 @@ describe('Test references for all kinds of arguments', () => { const UNRESOLVABLE = {}; function compileAndResolve(tInfo, refType, name, value) { - const scope = new Scope(); - let obj = { [name]: value }; - if (refType === '$var') { - if (value !== UNRESOLVABLE) { - scope.resources = obj; - } - obj = {}; - } else if (refType !== '$path') { - throw new Error(`Fix your test!!!! Unknown refType '${refType}. Expected either '$path' or '$var'.`); + if (refType !== '$var') { + throw new Error(`Fix your test!!!! Unknown refType '${refType}. Expected '$var'.`); } + const resources = name !== '$' && value !== UNRESOLVABLE ? { [name]: value } : {}; + const scope = new Scope(name === '$' ? value : {}, resources); const expr = tInfo.type.compile({ [refType]: name }); - tInfo.type.resolve(expr, scope, obj); + tInfo.type.resolve(expr, scope); if (expr.error) { throw new Error(expr.error); } @@ -38,7 +33,7 @@ describe('Test references for all kinds of arguments', () => { const testUnresolvedReferences = []; const mismatchedReferences = []; if (tInfo.acceptValueRef()) { - testRefToValues.push(good => compileAndResolve(tInfo, '$path', 'a', tInfo.value(good))); + testRefToValues.push(good => compileAndResolve(tInfo, '$var', '$', tInfo.value(good))); testRefToValues.push(good => compileAndResolve(tInfo, '$var', 'a', tInfo.value(good))); testUnresolvedReferences.push(() => compileAndResolve(tInfo, '$var', 'V1', UNRESOLVABLE)); mismatchedReferences.push({ $var: '$VALIDATOR' }); @@ -46,7 +41,6 @@ describe('Test references for all kinds of arguments', () => { if (tInfo.acceptValidatorRef()) { testRefToValues.push(good => compileAndResolve(tInfo, '$var', '$V1', tInfo.value(good))); testUnresolvedReferences.push(() => compileAndResolve(tInfo, '$var', '$V1', UNRESOLVABLE)); - mismatchedReferences.push({ $path: 'a' }); mismatchedReferences.push({ $var: 'VARIABLE' }); } From 869ae6f8666ba9de64ef529376a260af6bf5898f Mon Sep 17 00:00:00 2001 From: davebaol Date: Mon, 10 Jun 2019 20:06:07 +0200 Subject: [PATCH 02/16] Make public stuff easy accessible from index.js --- examples/data-driven.js | 5 ++--- examples/dsl-validator.js | 5 ++--- examples/hard-coded.js | 5 ++--- src/ensure-validator.js | 9 --------- src/index.js | 22 +++++++++++++++------ src/util/types.js | 2 +- test/branch-validators/call.js | 2 +- test/branch-validators/def.js | 3 +-- test/branch-validators/if.js | 3 +-- test/branch-validators/iterators.js | 3 +-- test/branch-validators/logical-operators.js | 2 +- test/branch-validators/misc.js | 3 +-- test/leaf-validators/equals.js | 3 +-- test/leaf-validators/isLength.js | 3 +-- test/leaf-validators/isOneOf.js | 2 +- test/leaf-validators/isPort.js | 2 +- test/leaf-validators/isSet.js | 2 +- test/leaf-validators/type-checkers.js | 2 +- test/test-utils.js | 3 +-- test/util/create-shortcuts.js | 3 +-- test/util/info.js | 2 +- test/util/scope.js | 3 +-- test/util/types/child.js | 2 +- test/util/types/native.js | 2 +- 24 files changed, 41 insertions(+), 52 deletions(-) delete mode 100644 src/ensure-validator.js diff --git a/examples/data-driven.js b/examples/data-driven.js index 8738ae2..54a25ce 100644 --- a/examples/data-driven.js +++ b/examples/data-driven.js @@ -2,8 +2,7 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); -const ensureValidator = require("../lib/ensure-validator"); -const Scope = require("../lib/util/scope"); +const { Scope, compile } = require("../lib"); let toBeValidated = { a: { @@ -15,7 +14,7 @@ let toBeValidated = { // Load validator from file let vObj = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "data-driven.yaml"), 'utf8')); -let validator = ensureValidator(vObj); +let validator = compile(vObj); // Validate let vError = validator(new Scope(toBeValidated)); diff --git a/examples/dsl-validator.js b/examples/dsl-validator.js index 46e9f95..3182097 100644 --- a/examples/dsl-validator.js +++ b/examples/dsl-validator.js @@ -2,12 +2,11 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); -const ensureValidator = require("../lib/ensure-validator"); -const Scope = require("../lib/util/scope"); +const { Scope, compile } = require("../lib"); // Load DSL validator from file let dslValidator = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "dsl-validator.yaml"), 'utf8')); -let validator = ensureValidator(dslValidator); +let validator = compile(dslValidator); // Validate the DSL validator itself let toBeValidated = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "dsl-validator.yaml"), 'utf8')); diff --git a/examples/hard-coded.js b/examples/hard-coded.js index 2790fec..e3c3450 100644 --- a/examples/hard-coded.js +++ b/examples/hard-coded.js @@ -1,7 +1,6 @@ /* eslint-disable no-console */ -const path = require("path"); -const V = require('../lib'); -const Scope = require("../lib/util/scope"); +const path = require('path'); +const { V, Scope } = require('../lib'); let toBeValidated = { a: { diff --git a/src/ensure-validator.js b/src/ensure-validator.js deleted file mode 100644 index 034afb8..0000000 --- a/src/ensure-validator.js +++ /dev/null @@ -1,9 +0,0 @@ -const child = require('./util/types').getNativeType('child'); - -module.exports = (v) => { - const expr = child.compile(v); - if (expr.resolved) { - return expr.result; - } - throw new Error('Expected a validator; found a reference instead'); -}; diff --git a/src/index.js b/src/index.js index 8ef159a..e7984a4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,15 +1,25 @@ -const leafValidators = require('./leaf-validators'); -const branchValidators = require('./branch-validators'); - /* CAUTION!!! + The two lines below are apparently useless, but they're not. Because of the following circular dependency index.js --require--> branch-validators.js --require--> util/types.js --require--> index.js the line below - module.exports = Object.assign({}, leafValidators, branchValidators); + module.exports.V = Object.assign({}, leafValidators, branchValidators); was creating an issue in util/types.js where the object exported from here appeared to be empty i.e. {} - The solution is to add properties to it in place, rather than re-assigning + The solution is to set property V to an empty object before triggering the circular dependency + and then add properties to it in place through Object.assign(), rather than re-assigning a new object to it. */ -Object.assign(module.exports, leafValidators, branchValidators); +module.exports.V = {}; +Object.assign(module.exports.V, require('./leaf-validators'), require('./branch-validators')); +module.exports.Scope = require('./util/scope'); +module.exports.Context = require('./util/context'); +// module.exports.compile = require('./ensure-validator'); +const child = require('./util/types').getNativeType('child'); + +module.exports.compile = (v) => { + const expr = child.compile(v); + if (expr.resolved) { return expr.result; } + throw new Error('Expected a validator; found a reference instead'); +}; diff --git a/src/util/types.js b/src/util/types.js index ccab75f..72ff3bc 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -3,7 +3,7 @@ const isRegExp = require('is-regexp'); const { BAD_PATH, ensureArrayPath } = require('./path'); const Expression = require('./expression'); const { ANY_VALUE, checkUniqueKey, lazyProperty } = require('./misc'); -const V = require('..'); +const { V } = require('..'); // Primitive and union types are progressively added below const NATIVE_TYPES = {}; diff --git a/test/branch-validators/call.js b/test/branch-validators/call.js index 39932c0..fe634e5 100644 --- a/test/branch-validators/call.js +++ b/test/branch-validators/call.js @@ -1,4 +1,4 @@ -import V from '../../src'; +import { V } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/branch-validators/def.js b/test/branch-validators/def.js index 6e6e055..e25417f 100644 --- a/test/branch-validators/def.js +++ b/test/branch-validators/def.js @@ -1,6 +1,5 @@ import { assert } from 'chai'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/branch-validators/if.js b/test/branch-validators/if.js index b43a5ba..0fe6063 100644 --- a/test/branch-validators/if.js +++ b/test/branch-validators/if.js @@ -1,6 +1,5 @@ import { assert } from 'chai'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { FAILURE } = VALIDATION; diff --git a/test/branch-validators/iterators.js b/test/branch-validators/iterators.js index 4ab8b22..15fc503 100644 --- a/test/branch-validators/iterators.js +++ b/test/branch-validators/iterators.js @@ -1,7 +1,6 @@ import { assert } from 'chai'; import lengthOf from '@davebaol/length-of'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/branch-validators/logical-operators.js b/test/branch-validators/logical-operators.js index 1cec906..1b1f40a 100644 --- a/test/branch-validators/logical-operators.js +++ b/test/branch-validators/logical-operators.js @@ -1,4 +1,4 @@ -import V from '../../src'; +import { V } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/branch-validators/misc.js b/test/branch-validators/misc.js index ae6b50e..fd5ad64 100644 --- a/test/branch-validators/misc.js +++ b/test/branch-validators/misc.js @@ -1,6 +1,5 @@ import { assert } from 'chai'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS } = VALIDATION; diff --git a/test/leaf-validators/equals.js b/test/leaf-validators/equals.js index 42912dc..497c576 100644 --- a/test/leaf-validators/equals.js +++ b/test/leaf-validators/equals.js @@ -1,7 +1,6 @@ import { assert } from 'chai'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/leaf-validators/isLength.js b/test/leaf-validators/isLength.js index e93db46..524d3b9 100644 --- a/test/leaf-validators/isLength.js +++ b/test/leaf-validators/isLength.js @@ -1,6 +1,5 @@ import { assert } from 'chai'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/leaf-validators/isOneOf.js b/test/leaf-validators/isOneOf.js index 15b72d6..175159a 100644 --- a/test/leaf-validators/isOneOf.js +++ b/test/leaf-validators/isOneOf.js @@ -1,5 +1,5 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V } from '../../src'; const { SUCCESS, FAILURE, THROW } = VALIDATION; diff --git a/test/leaf-validators/isPort.js b/test/leaf-validators/isPort.js index e667b3f..503f647 100644 --- a/test/leaf-validators/isPort.js +++ b/test/leaf-validators/isPort.js @@ -1,5 +1,5 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V } from '../../src'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/leaf-validators/isSet.js b/test/leaf-validators/isSet.js index a727ba1..5804c5d 100644 --- a/test/leaf-validators/isSet.js +++ b/test/leaf-validators/isSet.js @@ -1,5 +1,5 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V } from '../../src'; const { SUCCESS, FAILURE } = VALIDATION; diff --git a/test/leaf-validators/type-checkers.js b/test/leaf-validators/type-checkers.js index d2c4c6e..484dbaa 100644 --- a/test/leaf-validators/type-checkers.js +++ b/test/leaf-validators/type-checkers.js @@ -1,5 +1,5 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V } from '../../src'; const { SUCCESS, FAILURE, THROW } = VALIDATION; diff --git a/test/test-utils.js b/test/test-utils.js index 45c9357..52fceae 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,8 +1,7 @@ import { assert } from 'chai'; import Expression from '../src/util/expression'; -import Scope from '../src/util/scope'; import { clone } from '../src/util/misc'; -import V from '../src'; +import { V, Scope } from '../src'; const UNKNOWN_REF = Object.freeze({ $unknownRefType: 'anything' }); diff --git a/test/util/create-shortcuts.js b/test/util/create-shortcuts.js index 625cdf8..83f835a 100644 --- a/test/util/create-shortcuts.js +++ b/test/util/create-shortcuts.js @@ -1,7 +1,6 @@ import { assert } from 'chai'; import camelcase from 'camelcase'; -import V from '../../src'; -import Scope from '../../src/util/scope'; +import { V, Scope } from '../../src'; import createShortcuts from '../../src/util/create-shortcuts'; describe('Test shortcut opt.', () => { diff --git a/test/util/info.js b/test/util/info.js index f254bea..c62056e 100644 --- a/test/util/info.js +++ b/test/util/info.js @@ -1,7 +1,7 @@ import { assert } from 'chai'; import Info from '../../src/util/info'; import Argument from '../../src/util/argument'; -import V from '../../src'; +import { V } from '../../src'; describe('Test Info instance creation.', () => { function validator(name) { diff --git a/test/util/scope.js b/test/util/scope.js index fad535a..7421a30 100644 --- a/test/util/scope.js +++ b/test/util/scope.js @@ -1,7 +1,6 @@ import { assert } from 'chai'; -import Scope from '../../src/util/scope'; import Expression from '../../src/util/expression'; -import V from '../../src'; +import { V, Scope } from '../../src'; describe('Test utility Scope.compile($, scope).', () => { it('Should throw an error if the scope is a root reference', () => { diff --git a/test/util/types/child.js b/test/util/types/child.js index d237851..5c82e18 100644 --- a/test/util/types/child.js +++ b/test/util/types/child.js @@ -1,6 +1,6 @@ import { assert } from 'chai'; import { getNativeType } from '../../../src/util/types'; -import V from '../../../src'; +import { V } from '../../../src'; describe('Test ChildType.compile()', () => { const child = getNativeType('child'); diff --git a/test/util/types/native.js b/test/util/types/native.js index 2ecaa00..9d5926f 100644 --- a/test/util/types/native.js +++ b/test/util/types/native.js @@ -2,7 +2,7 @@ import { assert } from 'chai'; import { isRegExp } from 'util'; import getValue from 'get-value'; import types from '../../../src/util/types'; -import V from '../../../src'; +import { V } from '../../../src'; function merge(source, ...keys) { return keys.reduce((acc, k) => acc.concat(getValue(source, k)), []); From 076d83542c135bfcaaac043ca868760cd9716101 Mon Sep 17 00:00:00 2001 From: davebaol Date: Wed, 12 Jun 2019 17:07:31 +0200 Subject: [PATCH 03/16] Simplify union type members --- src/util/types.js | 67 ++++++++++++++++++++++--------------- test/util/types/get-type.js | 11 ++++-- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/util/types.js b/src/util/types.js index 72ff3bc..ac2e9c2 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -224,6 +224,36 @@ addNativeTypes(primitiveTypes); // --------------------- UNION TYPES --------------------- // ------------------------------------------------------- +function updateUniqueMembers(members, tn) { + const t = NATIVE_TYPES[tn]; + if (!t) { + throw new Error(`Unknown native type '${tn}'`); + } + const v = members[tn]; + if (v === null || v === t) { + return; // already present or included by some other union type member + } + // Add current type + members[tn] = t; // eslint-disable-line no-param-reassign + if (t.members) { + for (let i = 0, len = t.members.length; i < len; i += 1) { + // Mark as included by current union type member + members[t.members[i].name] = null; // eslint-disable-line no-param-reassign + } + } +} + +function uniqueMembersToArray(members) { + const out = []; + // eslint-disable-next-line no-restricted-syntax + for (const k in members) { + if (members[k] !== null) { + out.push(members[k]); + } + } + return out; +} + class UnionType extends Type { constructor(name, members, score) { super(name, score); @@ -248,36 +278,19 @@ class UnionType extends Type { } else if (!Array.isArray(members)) { throw new Error(`Expected either a '|' separated string or an array of native types; found ${members}`); } - let nullable = false; + + const uniqueMembers = {}; for (let i = 0; i < m.length; i += 1) { - const tn = m[i]; - const qmIndex = tn.lastIndexOf('?'); - if (qmIndex >= 0) { - if (m === members) { // Make a shallow copy of input array before changing any item - m = Array.from(m); - } - m[i] = tn.substring(0, qmIndex); - nullable = true; + const tn = m[i].trim(); + if (tn.endsWith('?')) { + updateUniqueMembers(uniqueMembers, 'null'); + updateUniqueMembers(uniqueMembers, tn.substring(0, tn.length - 1).trim()); + } else { + updateUniqueMembers(uniqueMembers, tn); } } - if (nullable) { - m.push('null'); - } - let out = m.map((typeName) => { - const tn = typeName.trim(); - const t = NATIVE_TYPES[tn]; - if (t) { - return t; - } - throw new Error(`Unknown native type '${tn}'`); - }); - - // TODO here we should check for ambiguous things like string|path - // since string is also part of path and has a special treatment, see path.compile() - - out = out.sort((a, b) => a.score - b.score) - .filter((x, i, a) => !i || x !== a[i - 1]); // unique - + const out = uniqueMembersToArray(uniqueMembers) + .sort((a, b) => a.score - b.score); if (out.length === 0) { throw new Error('A union type must have at least one member'); } diff --git a/test/util/types/get-type.js b/test/util/types/get-type.js index d064a44..5a77695 100644 --- a/test/util/types/get-type.js +++ b/test/util/types/get-type.js @@ -43,16 +43,21 @@ describe('Test getType(typeDesc).', () => { const types = ' integer| STRING|child |boolean '; assert.throws(() => getType(types), Error, 'Unknown native type'); }); - it('Union types with either an optional type or null as member should be identic.', () => { + it('Union types with either an optional type or null as member should be distinct instances.', () => { const t1 = getType('integer?'); const t2 = getType('null|integer'); - assert.deepEqual(t1, t2, ':('); + assert(t1 !== t2, ':('); }); - it('Union types with either an optional type or null as member should be distinct instances.', () => { + it('Union types with either an optional type or null as member should be identic.', () => { const t1 = getType('integer?'); const t2 = getType('null|integer'); assert.deepEqual(t1, t2, ':('); }); + it('Union types with redundant type members should be simplified.', () => { + const t1 = getType('any'); + const t2 = getType('null|any|string?|boolean'); + assert.deepEqual(t1, t2, ':('); + }); it('Union types with the same members in different order should be identic.', () => { const types = ' integer| string|child ? |boolean '; const t1 = getType(types); From e4d2fb2205093ccdd4282ab9acee3b93ad681949 Mon Sep 17 00:00:00 2001 From: davebaol Date: Thu, 13 Jun 2019 11:05:24 +0200 Subject: [PATCH 04/16] Add tests for validator compilation --- src/index.js | 2 +- test/index.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 test/index.js diff --git a/src/index.js b/src/index.js index e7984a4..326aab1 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,7 @@ module.exports.V = {}; Object.assign(module.exports.V, require('./leaf-validators'), require('./branch-validators')); module.exports.Scope = require('./util/scope'); module.exports.Context = require('./util/context'); -// module.exports.compile = require('./ensure-validator'); + const child = require('./util/types').getNativeType('child'); module.exports.compile = (v) => { diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a4c70a7 --- /dev/null +++ b/test/index.js @@ -0,0 +1,14 @@ +import { assert } from 'chai'; +import { V, compile } from '../src'; + +describe('Test compile().', () => { + it('Compiling a hard-coded validator should return a function validator', () => { + assert(typeof compile(V.isSet(null)) === 'function', ':('); + }); + it('Compiling a non hard-coded validator should return a function validator', () => { + assert(typeof compile({ isSet: [null] }) === 'function', ':('); + }); + it('Compiling a reference to a non hard-coded validator should throw an error', () => { + assert.throws(() => compile({ $var: '$MyValidator' }), Error, 'Expected a validator; found a reference instead'); + }); +}); From 60fb8a8e608cf095cbe180ee914a4641498ca849 Mon Sep 17 00:00:00 2001 From: davebaol Date: Thu, 13 Jun 2019 15:33:13 +0200 Subject: [PATCH 05/16] Support validator variants --- examples/data-driven.yaml | 12 +- examples/dsl-validator.js | 4 +- examples/dsl-validator.yaml | 133 ++++++------ examples/hard-coded.js | 12 +- src/branch-validators.js | 97 ++++----- src/leaf-validators/bridge.js | 217 +++++++++++--------- src/leaf-validators/index.js | 136 ++++++------ src/util/create-shortcuts.js | 50 ----- src/util/info.js | 18 +- src/util/misc.js | 7 +- src/util/variants.js | 70 +++++++ test/branch-validators/call.js | 12 +- test/branch-validators/def.js | 30 +-- test/branch-validators/if.js | 2 +- test/branch-validators/iterators.js | 40 ++-- test/branch-validators/logical-operators.js | 4 +- test/branch-validators/misc.js | 2 +- test/leaf-validators/bridge.js | 85 ++++---- test/leaf-validators/equals.js | 16 +- test/leaf-validators/isLength.js | 10 +- test/leaf-validators/isOneOf.js | 10 +- test/leaf-validators/isPort.js | 10 +- test/leaf-validators/isSet.js | 10 +- test/leaf-validators/type-checkers.js | 48 ++--- test/util/create-shortcuts.js | 48 ----- test/util/variants.js | 58 ++++++ 26 files changed, 609 insertions(+), 532 deletions(-) delete mode 100644 src/util/create-shortcuts.js create mode 100644 src/util/variants.js delete mode 100644 test/util/create-shortcuts.js create mode 100644 test/util/variants.js diff --git a/examples/data-driven.yaml b/examples/data-driven.yaml index 4e11b3f..e9cdf2f 100644 --- a/examples/data-driven.yaml +++ b/examples/data-driven.yaml @@ -1,8 +1,8 @@ and: - - isType: [a, object] + - isType$: [a, object] - xor: - - isSet: [a.b] - - isSet: [a.c] - - optIsType: [a.b, number] - - optIsType: [a.c, boolean] - - isArrayOf: [a.d, string] + - isSet$: [a.b] + - isSet$: [a.c] + - optIsType$: [a.b, number] + - optIsType$: [a.c, boolean] + - isArrayOf$: [a.d, string] diff --git a/examples/dsl-validator.js b/examples/dsl-validator.js index 3182097..011b65e 100644 --- a/examples/dsl-validator.js +++ b/examples/dsl-validator.js @@ -2,7 +2,7 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); -const { Scope, compile } = require("../lib"); +const { V, Scope, compile } = require("../lib"); // Load DSL validator from file let dslValidator = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "dsl-validator.yaml"), 'utf8')); @@ -10,6 +10,6 @@ let validator = compile(dslValidator); // Validate the DSL validator itself let toBeValidated = yaml.safeLoad(fs.readFileSync(path.join(__dirname, "dsl-validator.yaml"), 'utf8')); -let vError = validator(new Scope(toBeValidated)); +let vError = validator(new Scope(toBeValidated, {validatorNames: Object.keys(V)})); console.log(`${path.basename(__filename)}: Validation result --> ${vError? vError : "OK!"}`); \ No newline at end of file diff --git a/examples/dsl-validator.yaml b/examples/dsl-validator.yaml index 9fe424f..fd7e85f 100644 --- a/examples/dsl-validator.yaml +++ b/examples/dsl-validator.yaml @@ -1,52 +1,61 @@ def: - $VALIDATOR: and: - - isType: ['', object] - - isLength: ['', {min: 1, max: 1}] + - isType$: ['', object] + - isLength$: ['', {min: 1, max: 1}] - xor: - - matches: [$var, ^\$.] # Validator variable name starts with $ followed by at least 1 char + - matches$: [$var, ^\$.] # Validator variable name starts with $ followed by at least 1 char - $var: $LEAF_VALIDATOR - $var: $BRANCH_VALIDATOR $LEAF_VALIDATOR: - every: + every$: - '' - and: - - isOneOf: [key, [contains, equals, isAfter, isAlpha, isAlphanumeric, isArrayOf, isAscii, isBase64, isAfter, isBefore, isBoolean, isByteLength, isCreditCard, isAfter, isDataURI, isDate, isDecimal, isDivisibleBy, isEmail, isEmpty, isFloat, isFQDN, isFullWidth, isHalfWidth, isHash, isHexadecimal, isHexColor, isIdentityCard, isIn, isInt, isIP, isIPRange, isISBN, isISIN, isISO31661Alpha2, isISO31661Alpha3, isISO8601, isISRC, isISSN, isJSON, isJWT, isLatLong, isLength, isLowercase, isMACAddress, isMagnetURI, isMD5, isMimeType, isMobilePhone, isMongoId, isMultibyte, isNumeric, isOneOf, isPort, isPostalCode, isRFC3339, isSet, isSurrogatePair, isType, isUppercase, isURL, isUUID, isVariableWidth, isWhitelisted, matches, optContains, optEquals, optIsAfter, optIsAlpha, optIsAlphanumeric, optIsArrayOf, optIsAscii, optIsBase64, optIsAfter, optIsBefore, optIsBoolean, optIsByteLength, optIsCreditCard, optIsAfter, optIsDataUri, optIsDate, optIsDecimal, optIsDivisibleBy, optIsEmail, optIsEmpty, optIsFloat, optIsFqdn, optIsFullWidth, optIsHalfWidth, optIsHash, optIsHexadecimal, optIsHexColor, optIsIdentityCard, optIsIn, optIsInt, optIsIp, optIsIpRange, optIsIsbn, optIsIsin, optIsIso31661Alpha2, optIsIso31661Alpha3, optIsIso8601, optIsIsrc, optIsIssn, optIsJson, optIsJwt, optIsLatLong, optIsLength, optIsLowercase, optIsMacAddress, optIsMagnetUri, optIsMd5, optIsMimeType, optIsMobilePhone, optIsMongoId, optIsMultibyte, optIsNumeric, optIsOneOf, optIsPort, optIsPostalCode, optIsRfc3339, optIsSet, optIsSurrogatePair, optIsType, optIsUppercase, optIsUrl, optIsUuid, optIsVariableWidth, optIsWhitelisted, optMatches]] - - isType: [value, array] - - isLength: [value, {min: 1}] - - call: [value.0, {$var: $ARG_PATH}] + - isOneOf$: [key, {$var: validatorNames}] # validator names are defined in the root scope and passed in by the code launching the validation + - isType$: [value, array] + - isLength$: [value, {min: 1}] + - call$: [value.0, {$var: $ARG_PATH}] $BRANCH_VALIDATOR: xor: - - call: [alter, {$var: $ARG_ONE_CHILD_AND_TWO_RESULTS}] - - call: [and, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] - - call: [call, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call: [def, {$var: $ARG_SCOPE_AND_ONE_CHILD}] - - call: [every, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call: [if, {$var: $ARG_TWO_OR_THREE_CHILDREN}] - - call: [not, {$var: $ARG_ONE_CHILD}] - - call: [optCall, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call: [optEvery, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call: [optSome, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call: [optWhile, {$var: $ARG_PATH_AND_TWO_CHILDREN}] - - call: [or, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] - - call: [some, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call: [while, {$var: $ARG_PATH_AND_TWO_CHILDREN}] - - call: [xor, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] + - call$: [alter, {$var: $ARG_ONE_CHILD_AND_TWO_RESULTS}] + - call$: [and, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] + - call$: [call, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [call$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [def, {$var: $ARG_SCOPE_AND_ONE_CHILD}] + - call$: [every, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [every$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [if, {$var: $ARG_TWO_OR_THREE_CHILDREN}] + - call$: [not, {$var: $ARG_ONE_CHILD}] + - call$: [optCall, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optCall$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optEvery, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optEvery$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optSome, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optSome$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optWhile, {$var: $ARG_PATH_AND_TWO_CHILDREN}] + - call$: [optWhile$, {$var: $ARG_PATH_AND_TWO_CHILDREN}] + - call$: [or, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] + - call$: [some, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [some$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [while, {$var: $ARG_PATH_AND_TWO_CHILDREN}] + - call$: [while$, {$var: $ARG_PATH_AND_TWO_CHILDREN}] + - call$: [xor, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] $PATH: or: - - isType: ['', [string, number, 'null']] - - isArrayOf: ['', [string, number]] + - isType$: ['', [string, number, 'null']] + - isArrayOf$: ['', [string, number]] + - optIsSet$: [''] # Always true!!!! Temporary hack to be removed. $ARG_REF: and: - - isType: ['', object] - - isLength: ['', {min: 1, max: 1}] + - isType$: ['', object] + - isLength$: ['', {min: 1, max: 1}] - xor: - - matches: [$var, '^[^\$]'] # Non validator variable name cannot start with $ - - call: [$path, {$var: $PATH}] + - matches$: [$var, '^[^\$]'] # Non validator variable name cannot start with $ + - call$: [$path, {$var: $PATH}] $ARG_PATH: or: @@ -57,66 +66,66 @@ def: or: #- $var: $ARG_REF # Does it make sense to reference a scope? - and: - - isType: ['', object] - - every: + - isType$: ['', object] + - every$: - '' - if: - - matches: [key, ^\$.] - - call: [value, {$var: $VALIDATOR}] + - matches$: [key, ^\$.] + - call$: [value, {$var: $VALIDATOR}] $ARG_STRING: or: - $var: $ARG_REF - - isType: ['', string] + - isType$: ['', string] $ARG_ONE_CHILD_AND_TWO_RESULTS: and: - - isType: ['', array] - - call: [0, {$var: $VALIDATOR}] - - optCall: [1, {$var: $ARG_STRING}] - - optCall: [2, {$var: $ARG_STRING}] + - isType$: ['', array] + - call$: [0, {$var: $VALIDATOR}] + - optCall$: [1, {$var: $ARG_STRING}] + - optCall$: [2, {$var: $ARG_STRING}] $ARG_SCOPE_AND_ONE_CHILD: and: - - isType: ['', array] - - isLength: ['', {min: 2, max: 2}] - - optCall: [0, {$var: $ARG_SCOPE}] - - call: [1, {$var: $VALIDATOR}] + - isType$: ['', array] + - isLength$: ['', {min: 2, max: 2}] + - optCall$: [0, {$var: $ARG_SCOPE}] + - call$: [1, {$var: $VALIDATOR}] $ARG_PATH_AND_ONE_CHILD: and: - - isType: ['', array] - - isLength: ['', {min: 2, max: 2}] - - call: [0, {$var: $ARG_PATH}] - - call: [1, {$var: $VALIDATOR}] + - isType$: ['', array] + - isLength$: ['', {min: 2, max: 2}] + - call$: [0, {$var: $ARG_PATH}] + - call$: [1, {$var: $VALIDATOR}] $ARG_PATH_AND_TWO_CHILDREN: and: - - isType: ['', array] - - isLength: ['', {min: 3, max: 3}] - - call: [0, {$var: $ARG_PATH}] - - call: [1, {$var: $VALIDATOR}] - - call: [2, {$var: $VALIDATOR}] + - isType$: ['', array] + - isLength$: ['', {min: 3, max: 3}] + - call$: [0, {$var: $ARG_PATH}] + - call$: [1, {$var: $VALIDATOR}] + - call$: [2, {$var: $VALIDATOR}] $ARG_ZERO_OR_MORE_CHILDREN: and: - - isType: ['', array] - - every: + - isType$: ['', array] + - every$: - '' - - call: [value, {$var: $VALIDATOR}] + - call$: [value, {$var: $VALIDATOR}] $ARG_ONE_CHILD: and: - - isType: ['', array] - - isLength: ['', {min: 1, max: 1}] - - call: [0, {$var: $VALIDATOR}] + - isType$: ['', array] + - isLength$: ['', {min: 1, max: 1}] + - call$: [0, {$var: $VALIDATOR}] $ARG_TWO_OR_THREE_CHILDREN: and: - - isType: ['', array] - - isLength: ['', {min: 2, max: 3}] - - call: [0, {$var: $VALIDATOR}] - - call: [1, {$var: $VALIDATOR}] - - optCall: [2, {$var: $VALIDATOR}] + - isType$: ['', array] + - isLength$: ['', {min: 2, max: 3}] + - call$: [0, {$var: $VALIDATOR}] + - call$: [1, {$var: $VALIDATOR}] + - optCall$: [2, {$var: $VALIDATOR}] - $var: $VALIDATOR diff --git a/examples/hard-coded.js b/examples/hard-coded.js index e3c3450..8ee1ba6 100644 --- a/examples/hard-coded.js +++ b/examples/hard-coded.js @@ -12,14 +12,14 @@ let toBeValidated = { // Hard-coded validator let validator = V.and( // Rule 1 - V.isType("a", "object"), // Rule 2 + V.isType$("a", "object"), // Rule 2 V.xor( // Rule 3 - V.isSet("a.b"), - V.isSet("a.c") + V.isSet$("a.b"), + V.isSet$("a.c") ), - V.optIsType("a.b", "number"), // Rule 4 - V.optIsType("a.c", "boolean"), // Rule 5 - V.isArrayOf("a.d", "string") // Rule 6 + V.optIsType$("a.b", "number"), // Rule 4 + V.optIsType$("a.c", "boolean"), // Rule 5 + V.isArrayOf$("a.d", "string") // Rule 6 ); diff --git a/src/branch-validators.js b/src/branch-validators.js index 88b4fcb..d9c7435 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -1,27 +1,25 @@ -const { get } = require('./util/path'); const Scope = require('./util/scope'); -const createShortcuts = require('./util/create-shortcuts'); -const Info = require('./util/info'); +const { infoVariant, infoVariants$ } = require('./util/variants'); // // BRANCH VALIDATORS -// They all take child validators as arguments. +// They all take at least one child validator as arguments. // -function call(path, child) { - const infoArgs = call.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function call(info, arg, child) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const cExpr = infoArgs[1].compile(child); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!cExpr.resolved) { infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } - scope.context.push$(get(scope.find('$'), pExpr.result)); + scope.context.push$(info.getValue(aExpr, scope)); const result = cExpr.result(scope); scope.context.pop$(); return result; @@ -143,21 +141,21 @@ function _if(condChild, thenChild, elseChild) { }; } -function every(path, child) { - const infoArgs = every.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function every(info, arg, child) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const cExpr = infoArgs[1].compile(child); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!cExpr.resolved) { infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const $ = scope.find('$'); - const value = get($, pExpr.result); + const value = info.getValue(aExpr, scope); if (Array.isArray(value)) { const new$ = { original: $ }; scope.context.push$(new$); @@ -199,25 +197,25 @@ function every(path, child) { scope.context.pop$(); return error; } - return `every: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}'`; + return `every: the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`; }; } -function some(path, child) { - const infoArgs = some.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function some(info, arg, child) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const cExpr = infoArgs[1].compile(child); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!cExpr.resolved) { infoArgs[1].resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const $ = scope.find('$'); - const value = get($, pExpr.result); + const value = info.getValue(aExpr, scope); if (Array.isArray(value)) { const new$ = { original: $ }; scope.context.push$(new$); @@ -259,7 +257,7 @@ function some(path, child) { scope.context.pop$(); return error; } - return `some: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}' instead`; + return `some: the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}' instead`; }; } @@ -305,15 +303,15 @@ function onError(result, child) { } // eslint-disable-next-line no-underscore-dangle -function _while(path, condChild, doChild) { - const infoArgs = _while.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function _while(info, arg, condChild, doChild) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const ccExpr = infoArgs[1].compile(condChild); const dcExpr = infoArgs[2].compile(doChild); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!ccExpr.resolved) { infoArgs[1].resolve(ccExpr, scope); @@ -324,7 +322,7 @@ function _while(path, condChild, doChild) { if (dcExpr.error) { return dcExpr.error; } } const $ = scope.find('$'); - const value = get($, pExpr.result); + const value = info.getValue(aExpr, scope); const status = { succeeded: 0, failed: 0, original: $ }; if (Array.isArray(value)) { scope.context.push$(status); @@ -374,39 +372,34 @@ function _while(path, condChild, doChild) { scope.context.pop$(); return error; } - return `while: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}'`; + return `while: the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`; }; } function branchValidators() { /* eslint-disable no-unused-vars */ - /* istanbul ignore next */ const vInfo = [ - new Info(call, 'path:path', 'child:child'), - new Info(def, { def: 'scope:object', refDepth: -1 }, 'child:child'), - new Info(not, 'child:child'), - new Info(and, '...child:child'), - new Info(or, '...child:child'), - new Info(xor, '...child:child'), - new Info(_if, 'cond:child', 'then:child', 'else:child?'), - new Info(every, 'path:path', 'child:child'), - new Info(some, 'path:path', 'child:child'), - new Info(alter, 'resultOnSuccess:any?', 'resultOnError:any?', 'child:child'), - new Info(onError, 'result:any?', 'child:child'), - new Info(_while, 'path:path', 'cond:child', 'do:child') + ...infoVariants$(call, 'value:any', 'child:child'), + infoVariant(def, { def: 'scope:object', refDepth: -1 }, 'child:child'), + infoVariant(not, 'child:child'), + infoVariant(and, '...child:child'), + infoVariant(or, '...child:child'), + infoVariant(xor, '...child:child'), + infoVariant(_if, 'cond:child', 'then:child', 'else:child?'), + ...infoVariants$(every, 'value:any', 'child:child'), + ...infoVariants$(some, 'value:any', 'child:child'), + infoVariant(alter, 'resultOnSuccess:any?', 'resultOnError:any?', 'child:child'), + infoVariant(onError, 'result:any?', 'child:child'), + ...infoVariants$(_while, 'value:any', 'cond:child', 'do:child') ]; /* eslint-enable no-unused-vars */ const target = vInfo.reduce((acc, info) => { - info.consolidate(); const k = info.name; acc[k] = info.validator; // eslint-disable-line no-param-reassign return acc; }, {}); - // Augment with shortcut 'opt' all branch validators taking a path as first argument - createShortcuts(target, target, ['call', 'every', 'some', 'while']); - return target; } diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index 39f65f7..cff41e5 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -1,34 +1,34 @@ const v = require('validator'); -const { get } = require('../util/path'); +const { optInfoVariant } = require('../util/variants'); const Info = require('../util/info'); class Bridge extends Info { - constructor(name, errorFunc, ...noPathArgDescriptors) { - super(name, ...(['path:path'].concat(noPathArgDescriptors))); + constructor(name, errorFunc, ...argDescriptors) { + super(name, ...argDescriptors); this.errorFunc = errorFunc; } } +const EMPTY_OBJ = Object.freeze({}); + // These functions are optimizations specialized for certain types. // Their use avoids the conversion to and from string. const SPECIALIZED_VALIDATORS = { isDivisibleBy(value, number) { return value % number === 0; }, - isFloat(value, options) { - const opt = options || {}; - return (opt.min === undefined || value >= opt.min) - && (opt.max === undefined || value <= opt.max) - && (opt.lt === undefined || value < opt.lt) - && (opt.gt === undefined || value > opt.gt); + isFloat(value, options = EMPTY_OBJ) { + return (options.min === undefined || value >= options.min) + && (options.max === undefined || value <= options.max) + && (options.lt === undefined || value < options.lt) + && (options.gt === undefined || value > options.gt); }, - isInt(value, options) { - const opt = options || {}; + isInt(value, options = EMPTY_OBJ) { return Number.isInteger(value) - && (opt.min === undefined || value >= opt.min) - && (opt.max === undefined || value <= opt.max) - && (opt.lt === undefined || value < opt.lt) - && (opt.gt === undefined || value > opt.gt); + && (options.min === undefined || value >= options.min) + && (options.max === undefined || value <= options.max) + && (options.lt === undefined || value < options.lt) + && (options.gt === undefined || value > options.gt); }, isLatLong(value) { return value[0] >= -90 && value[0] <= 90 && value[1] >= -180 && value[1] <= 180; @@ -39,8 +39,16 @@ const SPECIALIZED_VALIDATORS = { }; class StringOnly extends Bridge { + static variants(name, errorFunc, ...argDescriptors) { + const adList = ['value:string', ...argDescriptors]; + return [ + new StringOnly(name, errorFunc, ...adList), + new StringOnly(`${name}$`, errorFunc, ...adList) + ]; + } + // eslint-disable-next-line class-methods-use-this - isSpecialized(value) { // eslint-disable-line no-unused-vars + isSpecializedFor(value) { // eslint-disable-line no-unused-vars return false; } @@ -54,43 +62,50 @@ class StringOnly extends Bridge { } link() { - const original = v[this.name]; - const specialized = SPECIALIZED_VALIDATORS[this.name]; - return (path, ...noPathArgs) => { - const pExpr = this.argDescriptors[0].compile(path); - const restExpr = this.compileRestParams(noPathArgs, 1); + const original = v[this.baseName]; + const specialized = SPECIALIZED_VALIDATORS[this.baseName]; + return (arg, ...restArgs) => { + const aExpr = this.argDescriptors[0].compile(arg); + const restExpr = this.compileRestParams(restArgs, 1); const restValue = []; return (scope) => { - if (!pExpr.resolved) { - this.argDescriptors[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + this.argDescriptors[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } const errorAt = this.resolveRestParams(restExpr, 1, scope); if (errorAt >= 0) { return restExpr[errorAt].error; } for (let i = 0, len = restExpr.length; i < len; i += 1) { restValue[i] = restExpr[i].result; } - let value = get(scope.find('$'), pExpr.result); + let value = this.getValue(aExpr, scope); let result; - if (specialized !== undefined && this.isSpecialized(value)) { + if (specialized !== undefined && this.isSpecializedFor(value)) { result = specialized(value, ...restValue); } else { value = this.cast(value); - if (typeof value === 'string') { - result = original(value, ...restValue); - } else { - return this.error(path); + if (typeof value !== 'string') { + return this.error(arg); } + result = original(value, ...restValue); } - return result ? undefined : this.error(path, noPathArgs); + return result ? undefined : this.error(arg, restArgs); }; }; } } -class StringAndNumber extends StringOnly { +class StringOrNumber extends StringOnly { + static variants(name, errorFunc, ...argDescriptors) { + const adList = ['value:string|number', ...argDescriptors]; + return [ + new StringOrNumber(name, errorFunc, ...adList), + new StringOrNumber(`${name}$`, errorFunc, ...adList) + ]; + } + // eslint-disable-next-line class-methods-use-this - isSpecialized(value) { + isSpecializedFor(value) { return typeof value === 'number'; } @@ -104,14 +119,22 @@ class StringAndNumber extends StringOnly { } } -class StringAndArray extends StringOnly { +class StringOrArray extends StringOnly { constructor(name, length, type, errorFunc, ...argDescriptors) { super(name, errorFunc, ...argDescriptors); this.length = length; this.type = type; } - isSpecialized(value) { + static variants(name, length, type, errorFunc, ...argDescriptors) { + const adList = ['value:string|array', ...argDescriptors]; + return [ + new StringOrArray(name, length, type, errorFunc, ...adList), + new StringOrArray(`${name}$`, length, type, errorFunc, ...adList) + ]; + } + + isSpecializedFor(value) { return Array.isArray(value) && (this.length === undefined || value.length === this.length) // eslint-disable-next-line valid-typeof @@ -132,65 +155,65 @@ function bridge(target) { /* eslint-disable no-unused-vars */ /* istanbul ignore next */ const vInfo = [ - new StringOnly('contains', args => `containing the value '${args[0]}'`, 'seed:string'), - // new StringOnly('equals', args => `equal to the value '${args[0]}'`), - // new StringOnly('isAfter', args => `equal to the value '${args[0]}'`), - new StringOnly('isAlpha', args => 'containing only letters (a-zA-Z)', 'locale:string?'), - new StringOnly('isAlphanumeric', args => 'containing only letters and numbers', 'locale:string?'), - new StringOnly('isAscii', args => 'containing ASCII chars only'), - new StringOnly('isBase64', args => 'base64 encoded'), - // new StringOnly('isBefore', args => `equal to the value '${args[0]}'`), - // new StringOnly('isBoolean', args => `equal to the value '${args[0]}'`), - new StringOnly('isByteLength', args => 'whose length (in UTF-8 bytes) falls in the specified range', 'options:object?'), - new StringOnly('isCreditCard', args => 'representing a credit card'), - new StringOnly('isCurrency', args => 'representing a valid currency amount', 'options:object?'), - new StringOnly('isDataURI', args => 'in data uri format'), - // new StringOnly('isDecimal', args => `equal to the value '${args[0]}'`), - new StringAndNumber('isDivisibleBy', args => `that's divisible by ${args[0]}`, 'divisor:integer'), - new StringOnly('isEmail', args => 'representing an email address', 'options:object?'), - new StringOnly('isEmpty', args => 'having a length of zero', 'options:object?'), - new StringAndNumber('isFloat', args => 'that\'s a float falling in the specified range', 'options:object?'), - new StringOnly('isFQDN', args => 'representing a fully qualified domain name (e.g. domain.com)', 'options:object?'), - new StringOnly('isFullWidth', args => 'containing any full-width chars'), - new StringOnly('isHalfWidth', args => 'containing any half-width chars'), - new StringOnly('isHash', args => `matching to the format of the hash algorithm ${args[0]}`, 'algorithm:string?'), - new StringOnly('isHexadecimal', args => 'representing a hexadecimal number'), - new StringOnly('isHexColor', args => 'matching to a hexadecimal color'), - new StringOnly('isIdentityCard', args => 'matching to a valid identity card code', 'locale:string?'), - // new StringOnly('isIn', args => `equal to the value '${args[0]}'`), - new StringAndNumber('isInt', args => 'that\'s an integer falling in the specified range', 'options:object?'), - new StringOnly('isIP', args => 'matching to an IP', 'version:integer?'), - new StringOnly('isIPRange', args => 'matching to an IP Range'), - new StringOnly('isISBN', args => 'matching to an ISBN', 'version:integer?'), - new StringOnly('isISIN', args => 'matching to an ISIN'), - new StringOnly('isISO31661Alpha2', args => 'matching to a valid ISO 3166-1 alpha-2 officially assigned country code'), - new StringOnly('isISO31661Alpha3', args => 'matching to a valid ISO 3166-1 alpha-3 officially assigned country code'), - new StringOnly('isISO8601', args => 'matching to a valid ISO 8601 date'), - new StringOnly('isISRC', args => 'matching to an ISRC'), - new StringOnly('isISSN', args => 'matching to an ISSN', 'options:object?'), - new StringOnly('isJSON', args => 'matching to a valid JSON'), - new StringOnly('isJWT', args => 'matching to a valid JWT token'), - new StringAndArray('isLatLong', 2, 'number', args => "representing a valid latitude-longitude coordinate in the format 'lat,long' or 'lat, long'"), - // new StringOnly('isLength', args => 'whose length falls in the specified range'), - new StringOnly('isLowercase', args => 'in lowercase'), - new StringOnly('isMACAddress', args => 'in MAC address format'), - new StringOnly('isMagnetURI', args => 'in magnet uri format'), - new StringOnly('isMD5', args => 'representing a valid MD5 hash'), - new StringOnly('isMimeType', args => 'matching to a valid MIME type format'), - new StringOnly('isMobilePhone', args => 'representing a mobile phone number', 'locale:string|array?', 'options:object?'), - new StringOnly('isMongoId', args => 'in the form of a valid hex-encoded representation of a MongoDB ObjectId.'), - new StringOnly('isMultibyte', args => 'containing one or more multibyte chars'), - new StringOnly('isNumeric', args => 'containing only numbers', 'options:object?'), - new StringAndNumber('isPort', args => 'representing a valid port'), - new StringOnly('isPostalCode', args => 'representing a postal code', 'options:object'), - new StringOnly('isRFC3339', args => 'matching to a valid RFC 3339 date'), - new StringOnly('isSurrogatePair', args => 'containing any surrogate pairs chars'), - new StringOnly('isUppercase', args => 'in uppercase'), - new StringOnly('isURL', args => 'representing a valid URL', 'options:object?'), - new StringOnly('isUUID', args => 'matching to a UUID (version 3, 4 or 5)', 'version:integer?'), - new StringOnly('isVariableWidth', args => 'containing a mixture of full and half-width chars'), - new StringOnly('isWhitelisted', args => 'whose characters belongs to the whitelist', 'chars:string'), - new StringOnly('matches', args => `matching the regex '${args[0]}'`, 'pattern:string|regex', 'modifiers:string?') + ...StringOnly.variants('contains', args => `containing the value '${args[0]}'`, 'seed:string'), + // ...StringOnly.variants('equals', args => `equal to the value '${args[0]}'`), + // ...StringOnly.variants('isAfter', args => `equal to the value '${args[0]}'`), + ...StringOnly.variants('isAlpha', args => 'containing only letters (a-zA-Z)', 'locale:string?'), + ...StringOnly.variants('isAlphanumeric', args => 'containing only letters and numbers', 'locale:string?'), + ...StringOnly.variants('isAscii', args => 'containing ASCII chars only'), + ...StringOnly.variants('isBase64', args => 'base64 encoded'), + // ...StringOnly.variants('isBefore', args => `equal to the value '${args[0]}'`), + // ...StringOnly.variants('isBoolean', args => `equal to the value '${args[0]}'`), + ...StringOnly.variants('isByteLength', args => 'whose length (in UTF-8 bytes) falls in the specified range', 'options:object?'), + ...StringOnly.variants('isCreditCard', args => 'representing a credit card'), + ...StringOnly.variants('isCurrency', args => 'representing a valid currency amount', 'options:object?'), + ...StringOnly.variants('isDataURI', args => 'in data uri format'), + // ...StringOnly.variants('isDecimal', args => `equal to the value '${args[0]}'`), + ...StringOrNumber.variants('isDivisibleBy', args => `that's divisible by ${args[0]}`, 'divisor:integer'), + ...StringOnly.variants('isEmail', args => 'representing an email address', 'options:object?'), + ...StringOnly.variants('isEmpty', args => 'having a length of zero', 'options:object?'), + ...StringOrNumber.variants('isFloat', args => 'that\'s a float falling in the specified range', 'options:object?'), + ...StringOnly.variants('isFQDN', args => 'representing a fully qualified domain name (e.g. domain.com)', 'options:object?'), + ...StringOnly.variants('isFullWidth', args => 'containing any full-width chars'), + ...StringOnly.variants('isHalfWidth', args => 'containing any half-width chars'), + ...StringOnly.variants('isHash', args => `matching to the format of the hash algorithm ${args[0]}`, 'algorithm:string?'), + ...StringOnly.variants('isHexadecimal', args => 'representing a hexadecimal number'), + ...StringOnly.variants('isHexColor', args => 'matching to a hexadecimal color'), + ...StringOnly.variants('isIdentityCard', args => 'matching to a valid identity card code', 'locale:string?'), + // ...StringOnly.variants('isIn', args => `equal to the value '${args[0]}'`), + ...StringOrNumber.variants('isInt', args => 'that\'s an integer falling in the specified range', 'options:object?'), + ...StringOnly.variants('isIP', args => 'matching to an IP', 'version:integer?'), + ...StringOnly.variants('isIPRange', args => 'matching to an IP Range'), + ...StringOnly.variants('isISBN', args => 'matching to an ISBN', 'version:integer?'), + ...StringOnly.variants('isISIN', args => 'matching to an ISIN'), + ...StringOnly.variants('isISO31661Alpha2', args => 'matching to a valid ISO 3166-1 alpha-2 officially assigned country code'), + ...StringOnly.variants('isISO31661Alpha3', args => 'matching to a valid ISO 3166-1 alpha-3 officially assigned country code'), + ...StringOnly.variants('isISO8601', args => 'matching to a valid ISO 8601 date'), + ...StringOnly.variants('isISRC', args => 'matching to an ISRC'), + ...StringOnly.variants('isISSN', args => 'matching to an ISSN', 'options:object?'), + ...StringOnly.variants('isJSON', args => 'matching to a valid JSON'), + ...StringOnly.variants('isJWT', args => 'matching to a valid JWT token'), + ...StringOrArray.variants('isLatLong', 2, 'number', args => "representing a valid latitude-longitude coordinate in the format 'lat,long' or 'lat, long'"), + // ...StringOnly.variants('isLength', args => 'whose length falls in the specified range'), + ...StringOnly.variants('isLowercase', args => 'in lowercase'), + ...StringOnly.variants('isMACAddress', args => 'in MAC address format'), + ...StringOnly.variants('isMagnetURI', args => 'in magnet uri format'), + ...StringOnly.variants('isMD5', args => 'representing a valid MD5 hash'), + ...StringOnly.variants('isMimeType', args => 'matching to a valid MIME type format'), + ...StringOnly.variants('isMobilePhone', args => 'representing a mobile phone number', 'locale:string|array?', 'options:object?'), + ...StringOnly.variants('isMongoId', args => 'in the form of a valid hex-encoded representation of a MongoDB ObjectId.'), + ...StringOnly.variants('isMultibyte', args => 'containing one or more multibyte chars'), + ...StringOnly.variants('isNumeric', args => 'containing only numbers', 'options:object?'), + ...StringOrNumber.variants('isPort', args => 'representing a valid port'), + ...StringOnly.variants('isPostalCode', args => 'representing a postal code', 'options:object'), + ...StringOnly.variants('isRFC3339', args => 'matching to a valid RFC 3339 date'), + ...StringOnly.variants('isSurrogatePair', args => 'containing any surrogate pairs chars'), + ...StringOnly.variants('isUppercase', args => 'in uppercase'), + ...StringOnly.variants('isURL', args => 'representing a valid URL', 'options:object?'), + ...StringOnly.variants('isUUID', args => 'matching to a UUID (version 3, 4 or 5)', 'version:integer?'), + ...StringOnly.variants('isVariableWidth', args => 'containing a mixture of full and half-width chars'), + ...StringOnly.variants('isWhitelisted', args => 'whose characters belongs to the whitelist', 'chars:string'), + ...StringOnly.variants('matches', args => `matching the regex '${args[0]}'`, 'pattern:string|regex', 'modifiers:string?') ]; /* eslint-enable no-unused-vars */ @@ -200,8 +223,12 @@ function bridge(target) { // 1. Make sure not to overwrite any function already defined in the target // 2. The value from the validator module must be a function (this prevents errors // due to changes in new versions of the module) - if (!(k in target) && typeof v[k] === 'function') { + if (!(k in target) && typeof v[info.baseName] === 'function') { target[k] = info.validator; // eslint-disable-line no-param-reassign + + // Add opt variant + const optInfo = optInfoVariant(info.validator); + target[optInfo.name] = optInfo.validator; // eslint-disable-line no-param-reassign } }); return target; diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 8d48457..9176ff7 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -1,49 +1,48 @@ const deepEqual = require('fast-deep-equal'); const lengthOf = require('@davebaol/length-of'); const bridge = require('./bridge'); -const { get } = require('../util/path'); -const createShortcuts = require('../util/create-shortcuts'); -const Info = require('../util/info'); +const { infoVariants$ } = require('../util/variants'); const { getType } = require('../util/types'); // // LEAF VALIDATORS -// They all take path as the first argument and have no children +// They all have no children and exist in 2 flavours: +// - standard: first argument is a value of type any +// - suffix $: first argument is a path in the object to validate // -function equals(path, value, deep) { - const infoArgs = equals.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const vExpr = infoArgs[1].compile(value); +function equals(info, arg, other, deep) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); + const oExpr = infoArgs[1].compile(other); const dExpr = infoArgs[2].compile(deep); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } - if (!vExpr.resolved) { - infoArgs[1].resolve(vExpr, scope); - if (vExpr.error) { return vExpr.error; } + if (!oExpr.resolved) { + infoArgs[1].resolve(oExpr, scope); + if (oExpr.error) { return oExpr.error; } } if (!dExpr.resolved) { infoArgs[2].resolve(dExpr, scope); if (dExpr.error) { return dExpr.error; } } - const result = dExpr.result - ? deepEqual(get(scope.find('$'), pExpr.result), vExpr.result) - : get(scope.find('$'), pExpr.result) === vExpr.result; - return result ? undefined : `equals: the value at path '${path}' must be equal to ${vExpr.result}`; + const value = info.getValue(aExpr, scope); + const result = dExpr.result ? deepEqual(value, oExpr.result) : value === oExpr.result; + return result ? undefined : `${info.name}: expected a value equal to ${oExpr.result}`; }; } -function isLength(path, options) { - const infoArgs = isLength.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function isLength(info, arg, options) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const optsExpr = infoArgs[1].compile(options); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!optsExpr.resolved) { infoArgs[1].resolve(optsExpr, scope); @@ -52,37 +51,37 @@ function isLength(path, options) { const opts = optsExpr.result; const min = opts.min || 0; const max = opts.max; // eslint-disable-line prefer-destructuring - const len = lengthOf(get(scope.find('$'), pExpr.result)); + const len = lengthOf(info.getValue(aExpr, scope)); if (len === undefined) { - return `isLength: the value at path '${path}' must be a string, an array or an object`; + return `${info.name}: expected a string, an array or an object`; } - return len >= min && (max === undefined || len <= max) ? undefined : `isLength: the value at path '${path}' must have a length between ${opts.min} and ${opts.max}`; + return len >= min && (max === undefined || len <= max) ? undefined : `${info.name}: expected string, array or object of length between ${opts.min} and ${opts.max}`; }; } -function isSet(path) { - const infoArgs = isSet.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function isSet(info, arg) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } - return get(scope.find('$'), pExpr.result) != null ? undefined : `isSet: the value at path '${path}' must be set`; + return info.getValue(aExpr, scope) != null ? undefined : `${info.name}: the value at path '${arg}' must be set`; }; } -function isType(path, type) { - const infoArgs = isType.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function isType(info, arg, type) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const tExpr = infoArgs[1].compile(type); if (tExpr.resolved) { tExpr.result = getType(tExpr.result); } return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!tExpr.resolved) { infoArgs[1].resolve(tExpr, scope); @@ -90,67 +89,65 @@ function isType(path, type) { try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } const t = tExpr.result; - return t.check(get(scope.find('$'), pExpr.result)) ? undefined : `isType: the value at path '${path}' must be a '${t.name}'`; + return t.check(info.getValue(aExpr, scope)) ? undefined : `${info.name}: the value at path '${arg}' must be a '${t.name}'`; }; } -function isOneOf(path, values) { - const infoArgs = isOneOf.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const aExpr = infoArgs[1].compile(values); +function isOneOf(info, arg, values) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); + const vExpr = infoArgs[1].compile(values); return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } - } if (!aExpr.resolved) { - infoArgs[1].resolve(aExpr, scope); + infoArgs[0].resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } - return aExpr.result.includes(get(scope.find('$'), pExpr.result)) ? undefined : `isOneOf: the value at path '${path}' must be one of ${aExpr.result}`; + if (!vExpr.resolved) { + infoArgs[1].resolve(vExpr, scope); + if (vExpr.error) { return vExpr.error; } + } + return vExpr.result.includes(info.getValue(aExpr, scope)) ? undefined : `${info.name}: the value at path '${arg}' must be one of ${aExpr.result}`; }; } -function isArrayOf(path, type) { - const infoArgs = isType.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); +function isArrayOf(info, arg, type) { + const infoArgs = info.argDescriptors; + const aExpr = infoArgs[0].compile(arg); const tExpr = infoArgs[1].compile(type); if (tExpr.resolved) { tExpr.result = getType(tExpr.result); } return (scope) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } + if (!aExpr.resolved) { + infoArgs[0].resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } } if (!tExpr.resolved) { infoArgs[1].resolve(tExpr, scope); if (tExpr.error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } - const value = get(scope.find('$'), pExpr.result); + const value = info.getValue(aExpr, scope); const t = tExpr.result; - if (!Array.isArray(value)) return `isArrayOf: the value at path '${path}' must be an array`; + if (!Array.isArray(value)) return `${info.name}: the value at path '${arg}' must be an array`; const flag = value.every(e => t.check(e)); - return flag ? undefined : `isArrayOf: the value at path '${path}' must be an array of '${t.name}'`; + return flag ? undefined : `${info.name}: the value at path '${arg}' must be an array of '${t.name}'`; }; } function leafValidators() { /* eslint-disable no-unused-vars */ - /* istanbul ignore next */ const vInfo = [ - new Info(equals, 'path:path', 'value:any', 'deep:boolean?'), - new Info(isArrayOf, 'path:path', 'type:string|array'), - new Info(isLength, 'path:path', 'options:object?'), - new Info(isOneOf, 'path:path', 'values:array'), - new Info(isSet, 'path:path'), - new Info(isType, 'path:path', 'type:string|array') + ...infoVariants$(equals, 'value:any', 'other:any', 'deep:boolean?'), + ...infoVariants$(isArrayOf, 'value:any', 'type:string|array'), + ...infoVariants$(isLength, 'value:any', 'options:object?'), + ...infoVariants$(isOneOf, 'value:any', 'values:array'), + ...infoVariants$(isSet, 'value:any'), + ...infoVariants$(isType, 'value:any', 'type:string|array') ]; /* eslint-enable no-unused-vars */ const target = vInfo.reduce((acc, info) => { - info.consolidate(); const k = info.name; acc[k] = info.validator; // eslint-disable-line no-param-reassign return acc; @@ -159,9 +156,6 @@ function leafValidators() { // Augment leaf validators with the ones bridged from validator package bridge(target); - // Augment all leaf validators with shortcut 'opt' - createShortcuts(target, target); - return target; } diff --git a/src/util/create-shortcuts.js b/src/util/create-shortcuts.js deleted file mode 100644 index 8ea950a..0000000 --- a/src/util/create-shortcuts.js +++ /dev/null @@ -1,50 +0,0 @@ -const camelCase = require('camelcase'); -const { get } = require('./path'); -const Info = require('./info'); -const Argument = require('./argument'); - -function getFirstArgType(validator) { - const ads = validator.info.argDescriptors; - return ads.length > 0 ? ads[0].type : undefined; -} - -function optShortcutOf(validator, name) { - let info; - const optV = (path, ...args) => { - const argDescriptor0 = info.argDescriptors[0]; - const pExpr = argDescriptor0.compile(path); - return (scope) => { - if (!pExpr.resolved) { - argDescriptor0.resolve(pExpr, scope); - if (pExpr.error) { return pExpr.error; } - } - return (get(scope.find('$'), pExpr.result) ? validator(pExpr.result, ...args)(scope) : undefined); - }; - }; - Object.defineProperty(optV, 'name', { value: name, writable: false }); - info = new Info(optV, ...(validator.info.argDescriptors.map(ad => new Argument(ad)))); - info.consolidate(); - return info.validator; -} - -function addShortcutOpt(target, source, key) { - const newKey = camelCase(`opt ${key}`); - if (typeof source[key] !== 'function' || !source[key].info) { - throw new Error(`Key '${key}' must be a validator function in order to create its opt shortcut '${newKey}'; found ${typeof source[key]} insead`); - } - const firstArgType = getFirstArgType(source[key]); - if (firstArgType.name !== 'path') { - throw new Error(`Validator '${key}' must take a path as first argument in order to create its opt shortcut '${newKey}'; found '${firstArgType}' insead`); - } - // eslint-disable-next-line no-param-reassign - target[newKey] = optShortcutOf(source[key], newKey); - return target; -} - -function createShortcuts(target, source, keys) { - source = source || target; // eslint-disable-line no-param-reassign - keys = keys || Object.keys(source); // eslint-disable-line no-param-reassign - return keys.reduce((acc, key) => addShortcutOpt(acc, source, key), target); -} - -module.exports = createShortcuts; diff --git a/src/util/info.js b/src/util/info.js index ab34bcc..8ea35ed 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -1,4 +1,5 @@ const Argument = require('./argument'); +const { get } = require('../util/path'); class Info { constructor(validator, ...argDescriptors) { @@ -13,7 +14,18 @@ class Info { } else { throw new Error('Expected the function or its name as first argument'); } - this.argDescriptors = argDescriptors; // will be processed in consolidate() + this.is$ = this.name.endsWith('$'); + if (this.is$) { + this.baseName = this.name.substring(0, this.name.length - 1); + this.getValue = (expr, scope) => get(scope.find('$'), expr.result); + // will be processed in consolidate() + this.argDescriptors = ['path:path', ...argDescriptors.slice(1)]; + } else { + this.baseName = this.name; + this.getValue = expr => expr.result; + // will be processed in consolidate() + this.argDescriptors = argDescriptors; + } } compileRestParams(args, offset = 0) { @@ -57,6 +69,7 @@ class Info { processArgDescriptors(context) { const last = this.argDescriptors.length - 1; + this.isLeaf = true; this.argDescriptors = this.argDescriptors.map((d, i) => { let a; try { @@ -67,6 +80,9 @@ class Info { if (i < last && a.restParams) { throw new Error(`Validator '${this.name}' argument at index ${i}: rest parameter is legal only for the last argument`); } + if (a.type.acceptsValidator) { + this.isLeaf = false; + } return a; }); } diff --git a/src/util/misc.js b/src/util/misc.js index 78f7610..29720f7 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -25,6 +25,10 @@ function checkUniqueKey(obj) { return k0; } +function setFunctionName(f, n) { + return Object.defineProperty(f, 'name', { value: n, writable: false }); +} + // Call this function from inside a getter to create on the specified instance (usually // passed as 'this') a property with the same name that shadows the getter itself function lazyProperty(instance, key, value, writable, configurable) { @@ -36,5 +40,6 @@ module.exports = { ANY_VALUE, checkUniqueKey, clone, - lazyProperty + lazyProperty, + setFunctionName }; diff --git a/src/util/variants.js b/src/util/variants.js new file mode 100644 index 0000000..6152980 --- /dev/null +++ b/src/util/variants.js @@ -0,0 +1,70 @@ +const camelCase = require('camelcase'); +const Info = require('./info'); +const { setFunctionName } = require('./misc'); + +function optShortcut(validator) { + const optValidator = (arg, ...args) => { + const { info } = optValidator; + const argDescriptor0 = info.argDescriptors[0]; + const aExpr = argDescriptor0.compile(arg); + info.compileRestParams(args, 1); // Make sure other arguments compile correctly + return (scope) => { + if (!aExpr.resolved) { + argDescriptor0.resolve(aExpr, scope); + if (aExpr.error) { return aExpr.error; } + } + return (info.getValue(aExpr, scope) ? validator(aExpr.result, ...args)(scope) : undefined); + }; + }; + return setFunctionName(optValidator, camelCase(`opt ${validator.info.name}`)); +} + +function infoVariant(validator, ...argDescriptors) { + if (typeof validator !== 'function' || !validator.name) { + throw new Error('infoVariant: expected a named function'); + } + const info = new Info(validator, ...argDescriptors); + info.consolidate(); + return info; +} + +function optInfoVariant(validator) { + if (typeof validator !== 'function' || !validator.info || !Object.isFrozen(validator.info)) { + throw new Error('infoOptVariant: expected a validator whose info property is consolidated'); + } + const { argDescriptors } = validator.info; + const info = new Info(optShortcut(validator), ...[`${argDescriptors[0].name}:${argDescriptors[0].type.name}?`, ...argDescriptors.slice(1)]); + info.consolidate(); + return info; +} + +function infoVariants(validator, ...argDescriptors) { + return [ + infoVariant(validator, ...argDescriptors), + optInfoVariant(validator) + ]; +} + +function getVariant(name, commonImpl) { + const f = (...args) => commonImpl(f.info, ...args); + return setFunctionName(f, name); +} + +function infoVariants$(commonImpl, ...argDescriptors) { + const { name } = commonImpl; + const func = typeof commonImpl === 'function' ? commonImpl : commonImpl.func; + const validator = getVariant(name, func); + const validator$ = getVariant(`${name}$`, func); + return [ + ...infoVariants(validator, ...argDescriptors), + ...infoVariants(validator$, ...argDescriptors) + ]; +} + +module.exports = { + infoVariant, + infoVariants, + infoVariants$, + optInfoVariant, + optShortcut +}; diff --git a/test/branch-validators/call.js b/test/branch-validators/call.js index fe634e5..b3cca42 100644 --- a/test/branch-validators/call.js +++ b/test/branch-validators/call.js @@ -3,10 +3,10 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; -describe('Test branch validator call.', () => { - const args = ['', V.isSet('a')]; - testAllArguments(V.call, args); - testValidation(SUCCESS, { a: -3.14 }, V.call, 'a', { isType: ['', 'number'] }); - testValidation(FAILURE, { a: '-3.14' }, V.call, 'a', { isType: ['', 'number'] }); - testValidation(FAILURE, {}, V.call, 'a', { isType: ['', 'number'] }); +describe('Test branch validator call$.', () => { + const args = ['', V.isSet$('a')]; + testAllArguments(V.call$, args); + testValidation(SUCCESS, { a: -3.14 }, V.call$, 'a', { isType$: ['', 'number'] }); + testValidation(FAILURE, { a: '-3.14' }, V.call$, 'a', { isType$: ['', 'number'] }); + testValidation(FAILURE, {}, V.call$, 'a', { isType$: ['', 'number'] }); }); diff --git a/test/branch-validators/def.js b/test/branch-validators/def.js index e25417f..e3dee6d 100644 --- a/test/branch-validators/def.js +++ b/test/branch-validators/def.js @@ -6,24 +6,24 @@ const { SUCCESS, FAILURE } = VALIDATION; describe('Test branch validator def.', () => { it('Should throw immediately an error on bad variables', () => { - assert.throws(() => V.def('Bad variables', {}, { isType: ['', 'number'] }), Error); + assert.throws(() => V.def('Bad variables', {}, { isType$: ['', 'number'] }), Error); }); it('Should throw immediately an error on bad validators', () => { - assert.throws(() => V.def({}, 'Bad validators', { isType: ['', 'number'] }), Error); + assert.throws(() => V.def({}, 'Bad validators', { isType$: ['', 'number'] }), Error); }); - testAllArguments(V.def, [{}, V.optIsSet('')]); - testValidation([SUCCESS, FAILURE, SUCCESS], { a: -3.14 }, V.def, { v1: -3.14 }, { equals: ['a', { $var: 'v1' }] }); - testValidation(FAILURE, { a: 'not -3.14' }, V.def, { v1: -3.14 }, { equals: ['a', { $var: 'v1' }] }); - it('def({}, V.optIsSet("")) should always succeed just like its child', () => { - const v = V.def({}, V.optIsSet('')); + testAllArguments(V.def, [{}, V.optIsSet$('')]); + testValidation([SUCCESS, FAILURE, SUCCESS], { a: -3.14 }, V.def, { v1: -3.14 }, { equals$: ['a', { $var: 'v1' }] }); + testValidation(FAILURE, { a: 'not -3.14' }, V.def, { v1: -3.14 }, { equals$: ['a', { $var: 'v1' }] }); + it('def({}, V.optIsSet$("")) should always succeed just like its child', () => { + const v = V.def({}, V.optIsSet$('')); assert(v(new Scope({ a: 123 })) === undefined, ':('); }); - it('def({$TEST: V.optIsSet("")}, {$var: "$TEST"}) should always succeed just like its referenced hard-coded child', () => { - const v = V.def({ $TEST: V.optIsSet('') }, { $var: '$TEST' }); + it('def({$TEST: V.optIsSet$("")}, {$var: "$TEST"}) should always succeed just like its referenced hard-coded child', () => { + const v = V.def({ $TEST: V.optIsSet$('') }, { $var: '$TEST' }); assert(v(new Scope({ a: 123 })) === undefined, ':('); }); - it('def({$TEST: {optIsSet: [""]}, {$var: "$TEST"}) should always succeed just like its referenced soft-coded child', () => { - const v = V.def({ $TEST: { optIsSet: [''] } }, { $var: '$TEST' }); + it('def({$TEST: {optIsSet$: [""]}, {$var: "$TEST"}) should always succeed just like its referenced soft-coded child', () => { + const v = V.def({ $TEST: { optIsSet$: [''] } }, { $var: '$TEST' }); assert(v(new Scope({ a: 123 })) === undefined, ':('); }); it('Scope of inner def can reference variable of outer def', () => { @@ -31,7 +31,7 @@ describe('Test branch validator def.', () => { { v1: 123 }, V.def( { v2: { $var: 'v1' } }, - { equals: ['a', { $var: 'v2' }] } + { equals$: ['a', { $var: 'v2' }] } ) ); assert(v(new Scope({ a: 123 })) === undefined, ':('); @@ -42,7 +42,7 @@ describe('Test branch validator def.', () => { v1: 123, v2: { $var: 'v1' } }, - { equals: ['a', { $var: 'v2' }] } + { equals$: ['a', { $var: 'v2' }] } ); assert(v(new Scope({ a: 123 })) === undefined, ':('); }); @@ -52,7 +52,7 @@ describe('Test branch validator def.', () => { v2: { $var: 'v1' }, v1: 123 }, - { equals: ['a', { $var: 'v2' }] } + { equals$: ['a', { $var: 'v2' }] } ); assert(v(new Scope({ a: 123 })) !== undefined, ':('); }); @@ -60,7 +60,7 @@ describe('Test branch validator def.', () => { const v = V.def( { var1: 'a', - $validator1: { equals: [{ $var: 'var1' }, 123] } + $validator1: { equals$: [{ $var: 'var1' }, 123] } }, V.def( { var1: 'b' }, diff --git a/test/branch-validators/if.js b/test/branch-validators/if.js index 0fe6063..6df6b7d 100644 --- a/test/branch-validators/if.js +++ b/test/branch-validators/if.js @@ -5,7 +5,7 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { FAILURE } = VALIDATION; describe('Test branch validator if.', () => { - const success = { optIsSet: [''] }; + const success = { optIsSet: [null] }; const failure = { not: [success] }; const vThen = { onError: ['then', failure] }; const vElse = { onError: ['else', failure] }; diff --git a/test/branch-validators/iterators.js b/test/branch-validators/iterators.js index 15fc503..2efef0f 100644 --- a/test/branch-validators/iterators.js +++ b/test/branch-validators/iterators.js @@ -13,14 +13,14 @@ const test = { const testKeys = Object.keys(test); function testEveryOrSome(name) { - const isEvery = name === 'every'; + const isEvery = name === 'every$'; const successForEvery = () => (isEvery ? undefined : 'error'); const failureForEvery = () => (isEvery ? 'error' : undefined); describe(`Test branch validator ${name}.`, () => { const everyOrSome = V[name]; - const args = ['a', V.isSet('')]; + const args = ['a', V.isSet$('')]; testAllArguments(everyOrSome, args); - testValidation(SUCCESS, test.array, everyOrSome, '', { optIsSet: ['value'] }); + testValidation(SUCCESS, test.array, everyOrSome, '', { optIsSet$: ['value'] }); testKeys.forEach(t => it(`For ${t}s ${name} should ${isEvery ? 'fail at first invalid' : 'succeed at first valid'} iteration`, () => { let count = 0; const expected = 2; @@ -64,34 +64,34 @@ function testEveryOrSome(name) { }); } -testEveryOrSome('every'); +testEveryOrSome('every$'); -testEveryOrSome('some'); +testEveryOrSome('some$'); -describe('Test branch validator while.', () => { +describe('Test branch validator while$.', () => { const args = ['a', () => undefined, () => undefined]; - testAllArguments(V.while, args); + testAllArguments(V.while$, args); testKeys.forEach((t) => { // Should fail when the condition fails - const vCond = { isInt: ['failed', { min: 0, max: 1 }] }; // fails on 2nd failure of vDo - const vDo = { not: [{ optIsSet: ['value'] }] }; // always fails - testValidation(FAILURE, test, V.while, t, vCond, vDo); + const vCond = { isInt$: ['failed', { min: 0, max: 1 }] }; // fails on 2nd failure of vDo + const vDo = { not: [{ optIsSet$: ['value'] }] }; // always fails + testValidation(FAILURE, test, V.while$, t, vCond, vDo); }); testKeys.forEach((t) => { // Should succeed when the condition never fails`, () => { - const vCond = { isInt: ['failed', { min: 0, max: 0 }] }; // fails on 1st failure of vDo - const vDo = { optIsSet: ['value'] }; // never fails - testValidation(SUCCESS, test, V.while, t, vCond, vDo); + const vCond = { isInt$: ['failed', { min: 0, max: 0 }] }; // fails on 1st failure of vDo + const vDo = { optIsSet$: ['value'] }; // never fails + testValidation(SUCCESS, test, V.while$, t, vCond, vDo); }); function iterationChecker(type, expected) { it(`For ${type}s while should generate proper iteration objects`, () => { const actual = []; - const vCond = V.optIsSet(''); // always true + const vCond = V.optIsSet$(''); // always true const vDo = (scope) => { actual.push(Object.assign({}, scope.find('$'))); return undefined; }; - const v = V.while(type, vCond, vDo); + const v = V.while$(type, vCond, vDo); v(new Scope(test)); assert.deepEqual(actual, expected, ':('); }); @@ -107,8 +107,8 @@ describe('Test branch validator while.', () => { }))); const failureExpected = { numbers: 123, booleans: true }; - Object.keys(failureExpected).forEach(k => it(`For ${k} while should fail`, () => { - const v = V.while('', () => undefined, () => undefined); + Object.keys(failureExpected).forEach(k => it(`For ${k} while$ should fail`, () => { + const v = V.while$('', () => undefined, () => undefined); assert(v(new Scope(failureExpected[k])) !== undefined, ':('); })); @@ -123,10 +123,10 @@ describe('Test branch validator while.', () => { { name: 'e', parent: false } ] }; - const v = V.while( + const v = V.while$( 'relatives', - V.isInt('succeeded', { min: 0, max: 2 }), - V.equals('value.parent', true) + V.isInt$('succeeded', { min: 0, max: 2 }), + V.equals$('value.parent', true) ); const result = v(new Scope(person)); assert(shouldSucceed ? result === undefined : result !== undefined, ':('); diff --git a/test/branch-validators/logical-operators.js b/test/branch-validators/logical-operators.js index 1b1f40a..0bcfc3d 100644 --- a/test/branch-validators/logical-operators.js +++ b/test/branch-validators/logical-operators.js @@ -3,8 +3,8 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; -const success = { optIsSet: [''] }; -const failure = { not: [{ optIsSet: [''] }] }; +const success = { optIsSet$: [''] }; +const failure = { not: [{ optIsSet$: [''] }] }; describe('Test branch validator not.', () => { testAllArguments(V.not, [success]); diff --git a/test/branch-validators/misc.js b/test/branch-validators/misc.js index fd5ad64..df6716f 100644 --- a/test/branch-validators/misc.js +++ b/test/branch-validators/misc.js @@ -4,7 +4,7 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS } = VALIDATION; -const success = { optIsSet: [''] }; +const success = { optIsSet$: [''] }; const failure = { not: [success] }; describe('Test branch validator alter.', () => { diff --git a/test/leaf-validators/bridge.js b/test/leaf-validators/bridge.js index 6466695..e56d982 100644 --- a/test/leaf-validators/bridge.js +++ b/test/leaf-validators/bridge.js @@ -17,55 +17,58 @@ describe('Test bridged leaf validators.', () => { assert(Object.keys(bv).every(k => typeof bv[k] === 'function'), ':('); }); - const infoList = ['StringOnly', 'StringAndNumber', 'StringAndArray']; - it(`Info of all bridged leaf validators should belong to [${infoList.join(', ')}]`, () => { + const infoList = ['StringOnly', 'StringOrNumber', 'StringOrArray']; + it(`Info of all non-shortcut bridged leaf validators should belong to [${infoList.join(', ')}]`, () => { const result = Object.keys(bv).reduce((acc, k) => { - acc[bv[k].info.constructor.name] = true; + const infoCtorName = bv[k].info.constructor.name; + if (!k.startsWith('opt') || infoCtorName !== 'Info') { // skip opt shortcut + acc[bv[k].info.constructor.name] = true; + } return acc; }, {}); assert.hasAllKeys(result, infoList, ':('); }); - // Test matches to test regex type too - const { matches } = bv; - testOwnerClass(matches, 'StringOnly'); - testAllArguments(matches, ['a', '.*']); - testValidation(SUCCESS, { a: 'string' }, matches, 'a', '.*', 'i'); - testValidation(FAILURE, { a: 3 }, matches, 'a', '.*'); - testValidation([THROW, FAILURE], { a: 'string' }, matches, 'a', 123); + // Test matches$ to test regex type too + const { matches$ } = bv; + testOwnerClass(matches$, 'StringOnly'); + testAllArguments(matches$, ['a', '.*']); + testValidation(SUCCESS, { a: 'string' }, matches$, 'a', '.*', 'i'); + testValidation(FAILURE, { a: 3 }, matches$, 'a', '.*'); + testValidation([THROW, FAILURE], { a: 'string' }, matches$, 'a', 123); - const { isDivisibleBy } = bv; - testOwnerClass(isDivisibleBy, 'StringAndNumber'); - testAllArguments(isDivisibleBy, ['a', 2]); - testValidation(SUCCESS, { a: '24' }, isDivisibleBy, 'a', 2); - testValidation(SUCCESS, { a: 24 }, isDivisibleBy, 'a', 2); - testValidation(FAILURE, { a: true }, isDivisibleBy, 'a', 2); - testValidation([THROW, FAILURE], { a: 3 }, isDivisibleBy, 'a', true); + const { isDivisibleBy$ } = bv; + testOwnerClass(isDivisibleBy$, 'StringOrNumber'); + testAllArguments(isDivisibleBy$, ['a', 2]); + testValidation(SUCCESS, { a: '24' }, isDivisibleBy$, 'a', 2); + testValidation(SUCCESS, { a: 24 }, isDivisibleBy$, 'a', 2); + testValidation(FAILURE, { a: true }, isDivisibleBy$, 'a', 2); + testValidation([THROW, FAILURE], { a: 3 }, isDivisibleBy$, 'a', true); - const { isInt } = bv; - testOwnerClass(isInt, 'StringAndNumber'); - testAllArguments(isInt, ['a', {}]); - testValidation(SUCCESS, { a: '3' }, isInt, 'a'); - testValidation(SUCCESS, { a: 3 }, isInt, 'a'); - testValidation(FAILURE, { a: true }, isInt, 'a'); - testValidation([THROW, FAILURE], { a: 3 }, isInt, 'a', true); + const { isInt$ } = bv; + testOwnerClass(isInt$, 'StringOrNumber'); + testAllArguments(isInt$, ['a', {}]); + testValidation(SUCCESS, { a: '3' }, isInt$, 'a'); + testValidation(SUCCESS, { a: 3 }, isInt$, 'a'); + testValidation(FAILURE, { a: true }, isInt$, 'a'); + testValidation([THROW, FAILURE], { a: 3 }, isInt$, 'a', true); - const { isFloat } = bv; - testOwnerClass(isFloat, 'StringAndNumber'); - testAllArguments(isFloat, ['a', {}]); - testValidation(SUCCESS, { a: '3' }, isFloat, 'a'); - testValidation(SUCCESS, { a: 3 }, isFloat, 'a'); - testValidation(FAILURE, { a: true }, isFloat, 'a'); - testValidation([THROW, FAILURE], { a: 3 }, isFloat, 'a', true); + const { isFloat$ } = bv; + testOwnerClass(isFloat$, 'StringOrNumber'); + testAllArguments(isFloat$, ['a', {}]); + testValidation(SUCCESS, { a: '3' }, isFloat$, 'a'); + testValidation(SUCCESS, { a: 3 }, isFloat$, 'a'); + testValidation(FAILURE, { a: true }, isFloat$, 'a'); + testValidation([THROW, FAILURE], { a: 3 }, isFloat$, 'a', true); - const { isLatLong } = bv; - testOwnerClass(isLatLong, 'StringAndArray'); - testAllArguments(isLatLong, ['']); - testValidation(SUCCESS, { a: '+90.0, -127.554334' }, isLatLong, 'a'); - testValidation(SUCCESS, { a: [+90.0, -127.554334] }, isLatLong, 'a'); - testValidation(SUCCESS, { a: ['+90.0', '-127.554334'] }, isLatLong, 'a'); - testValidation(SUCCESS, { a: ['+90.0', -127.554334] }, isLatLong, 'a'); - testValidation(FAILURE, { a: ['+90.0'] }, isLatLong, 'a'); - testValidation(FAILURE, { a: ['+90.0', true] }, isLatLong, 'a'); - testValidation(FAILURE, { a: true }, isLatLong, 'a'); + const { isLatLong$ } = bv; + testOwnerClass(isLatLong$, 'StringOrArray'); + testAllArguments(isLatLong$, ['']); + testValidation(SUCCESS, { a: '+90.0, -127.554334' }, isLatLong$, 'a'); + testValidation(SUCCESS, { a: [+90.0, -127.554334] }, isLatLong$, 'a'); + testValidation(SUCCESS, { a: ['+90.0', '-127.554334'] }, isLatLong$, 'a'); + testValidation(SUCCESS, { a: ['+90.0', -127.554334] }, isLatLong$, 'a'); + testValidation(FAILURE, { a: ['+90.0'] }, isLatLong$, 'a'); + testValidation(FAILURE, { a: ['+90.0', true] }, isLatLong$, 'a'); + testValidation(FAILURE, { a: true }, isLatLong$, 'a'); }); diff --git a/test/leaf-validators/equals.js b/test/leaf-validators/equals.js index 497c576..6c0c364 100644 --- a/test/leaf-validators/equals.js +++ b/test/leaf-validators/equals.js @@ -7,25 +7,25 @@ const { SUCCESS, FAILURE } = VALIDATION; const successExpected = [[false, false], [0, 0], ['foo', 'foo']]; const failureExpected = [[false, true], [0, 1], ['foo', 'bar'], [{}, {}], [[], []]]; -describe('Test leaf validator equals.', () => { - testAllArguments(V.equals, ['', 2, false]); - successExpected.forEach(pair => testValidation(SUCCESS, { a: pair[0] }, V.equals, 'a', pair[1])); - failureExpected.forEach(pair => testValidation(FAILURE, { a: pair[0] }, V.equals, 'a', pair[1])); +describe('Test leaf validator equals$.', () => { + testAllArguments(V.equals$, ['', 2, false]); + successExpected.forEach(pair => testValidation(SUCCESS, { a: pair[0] }, V.equals$, 'a', pair[1])); + failureExpected.forEach(pair => testValidation(FAILURE, { a: pair[0] }, V.equals$, 'a', pair[1])); // Deep equal - [null, undefined].forEach(b => it(`Deep equal should ${b === null ? 'fail' : 'succeed'}`, () => { + [null, undefined].forEach(b => it(`Deep equality should ${b === null ? 'fail' : 'succeed'}`, () => { const obj1 = { a: 'hello', b: [undefined, { x: true }] }; const obj2 = { a: 'hello', b: [b, { x: true }] }; - const v = V.equals('', obj1, true); + const v = V.equals$('', obj1, true); const result = v(new Scope(obj2)); assert(b === null ? result !== undefined : result === undefined, ':('); })); // Deep equal with reference - it('Deep equal with reference should succeed', () => { + it('Deep equality with reference should succeed', () => { const obj1 = { a: 'hello' }; const obj2 = { a: 'hello' }; - const v = V.def({ deep: true }, V.equals('', obj1, { $var: 'deep' })); + const v = V.def({ deep: true }, V.equals$('', obj1, { $var: 'deep' })); assert(v(new Scope(obj2)) === undefined, ':('); }); }); diff --git a/test/leaf-validators/isLength.js b/test/leaf-validators/isLength.js index 524d3b9..09d3167 100644 --- a/test/leaf-validators/isLength.js +++ b/test/leaf-validators/isLength.js @@ -9,22 +9,22 @@ const failureExpected = ['12', [1, 2], { a: 1, b: 2 }, true, null, 123]; function check(val, shouldSucceed) { const obj = { a: val }; - testValidation(shouldSucceed ? SUCCESS : FAILURE, obj, V.isLength, 'a', { min: 3, max: 3 }); + testValidation(shouldSucceed ? SUCCESS : FAILURE, obj, V.isLength$, 'a', { min: 3, max: 3 }); } function checkRef(val, shouldSucceed) { const obj = { a: val, referenced: 3 }; const options = { min: { $var: '$.referenced' }, max: 3 }; - it(`isLength("a", ${JSON.stringify(options)}) should ${shouldSucceed ? 'succeed' : 'fail'} for ${JSON.stringify(obj)}`, () => { - const v = V.isLength('a', options); + it(`isLength$("a", ${JSON.stringify(options)}) should ${shouldSucceed ? 'succeed' : 'fail'} for ${JSON.stringify(obj)}`, () => { + const v = V.isLength$('a', options); const result = v(new Scope(obj)); assert(shouldSucceed ? result === undefined : result !== undefined, ':('); }); } -describe('Test leaf validator isLength.', () => { +describe('Test leaf validator isLength$.', () => { const args = ['', {}]; - testAllArguments(V.isLength, args); + testAllArguments(V.isLength$, args); successExpected.forEach(obj => check(obj, true)); failureExpected.forEach(obj => check(obj, false)); successExpected.forEach(obj => checkRef(obj, true)); diff --git a/test/leaf-validators/isOneOf.js b/test/leaf-validators/isOneOf.js index 175159a..a320acc 100644 --- a/test/leaf-validators/isOneOf.js +++ b/test/leaf-validators/isOneOf.js @@ -7,10 +7,10 @@ const successExpected = [1, '1', true, null]; const failureExpected = [2, '2', false, {}, []]; const successValues = Array.from(successExpected); -describe('Test leaf validator isOneOf.', () => { +describe('Test leaf validator isOneOf$.', () => { const args = ['', ['']]; - testAllArguments(V.isOneOf, args); - successExpected.forEach(val => testValidation(SUCCESS, { a: val }, V.isOneOf, 'a', successValues)); - failureExpected.forEach(val => testValidation(FAILURE, { a: val }, V.isOneOf, 'a', successValues)); - testValidation([THROW, FAILURE], { a: 'x' }, V.isOneOf, 'a', 'not an array'); + testAllArguments(V.isOneOf$, args); + successExpected.forEach(val => testValidation(SUCCESS, { a: val }, V.isOneOf$, 'a', successValues)); + failureExpected.forEach(val => testValidation(FAILURE, { a: val }, V.isOneOf$, 'a', successValues)); + testValidation([THROW, FAILURE], { a: 'x' }, V.isOneOf$, 'a', 'not an array'); }); diff --git a/test/leaf-validators/isPort.js b/test/leaf-validators/isPort.js index 503f647..abb0d63 100644 --- a/test/leaf-validators/isPort.js +++ b/test/leaf-validators/isPort.js @@ -6,9 +6,9 @@ const { SUCCESS, FAILURE } = VALIDATION; const successExpected = [8080, '8080', 0, '0', 65535, '65535']; const failureExpected = [-1, '-1', 65536, true, null]; -describe('Test leaf validator isPort.', () => { - testAllArguments(V.isPort, ['a']); - successExpected.forEach(val => testValidation(SUCCESS, { a: val }, V.isPort, 'a')); - failureExpected.forEach(val => testValidation(FAILURE, { a: val }, V.isPort, 'a')); - testValidation(FAILURE, {}, V.isPort, 'a'); +describe('Test leaf validator isPort$.', () => { + testAllArguments(V.isPort$, ['a']); + successExpected.forEach(val => testValidation(SUCCESS, { a: val }, V.isPort$, 'a')); + failureExpected.forEach(val => testValidation(FAILURE, { a: val }, V.isPort$, 'a')); + testValidation(FAILURE, {}, V.isPort$, 'a'); }); diff --git a/test/leaf-validators/isSet.js b/test/leaf-validators/isSet.js index 5804c5d..bb9d8ca 100644 --- a/test/leaf-validators/isSet.js +++ b/test/leaf-validators/isSet.js @@ -6,9 +6,9 @@ const { SUCCESS, FAILURE } = VALIDATION; const successExpected = [false, true, 0, 1, {}, []]; const failureExpected = [null]; -describe('Test leaf validator isSet.', () => { - testAllArguments(V.isSet, ['']); - successExpected.forEach(val => testValidation(SUCCESS, { a: val }, V.isSet, 'a')); - failureExpected.forEach(val => testValidation(FAILURE, { a: val }, V.isSet, 'a')); - testValidation(FAILURE, {}, V.isSet, 'a'); +describe('Test leaf validator isSet$.', () => { + testAllArguments(V.isSet$, ['']); + successExpected.forEach(val => testValidation(SUCCESS, { a: val }, V.isSet$, 'a')); + failureExpected.forEach(val => testValidation(FAILURE, { a: val }, V.isSet$, 'a')); + testValidation(FAILURE, {}, V.isSet$, 'a'); }); diff --git a/test/leaf-validators/type-checkers.js b/test/leaf-validators/type-checkers.js index 484dbaa..dd0f6fc 100644 --- a/test/leaf-validators/type-checkers.js +++ b/test/leaf-validators/type-checkers.js @@ -3,32 +3,32 @@ import { V } from '../../src'; const { SUCCESS, FAILURE, THROW } = VALIDATION; -describe('Test leaf validator isType.', () => { +describe('Test leaf validator isType$.', () => { const args = ['', 'string']; - testAllArguments(V.isType, args); - testValidation(SUCCESS, { a: '' }, V.isType, [], 'object'); - testValidation(SUCCESS, { a: '' }, V.isType, '', 'object'); - testValidation(SUCCESS, { a: false }, V.isType, 'a', 'boolean'); - testValidation(SUCCESS, { a: true }, V.isType, 'a', 'boolean'); - testValidation(FAILURE, { a: null }, V.isType, 'a', 'regex'); // take the opportunity to test regex type too - testValidation([THROW, FAILURE], { a: '...' }, V.isType, 'a', 'unknown_type'); - testValidation(SUCCESS, { a: false }, V.isType, 'a', ['boolean', 'array']); - testValidation(SUCCESS, { a: [] }, V.isType, 'a', ['boolean', 'array']); - testValidation(FAILURE, { a: '...' }, V.isType, 'a', ['boolean', 'array']); - testValidation([THROW, FAILURE], { a: '...' }, V.isType, 'a', ['unknown_type', 'array']); + testAllArguments(V.isType$, args); + testValidation(SUCCESS, { a: '' }, V.isType$, [], 'object'); + testValidation(SUCCESS, { a: '' }, V.isType$, '', 'object'); + testValidation(SUCCESS, { a: false }, V.isType$, 'a', 'boolean'); + testValidation(SUCCESS, { a: true }, V.isType$, 'a', 'boolean'); + testValidation(FAILURE, { a: null }, V.isType$, 'a', 'regex'); // take the opportunity to test regex type too + testValidation([THROW, FAILURE], { a: '...' }, V.isType$, 'a', 'unknown_type'); + testValidation(SUCCESS, { a: false }, V.isType$, 'a', ['boolean', 'array']); + testValidation(SUCCESS, { a: [] }, V.isType$, 'a', ['boolean', 'array']); + testValidation(FAILURE, { a: '...' }, V.isType$, 'a', ['boolean', 'array']); + testValidation([THROW, FAILURE], { a: '...' }, V.isType$, 'a', ['unknown_type', 'array']); }); -describe('Test leaf validator isArrayOf.', () => { +describe('Test leaf validator isArrayOf$.', () => { const args = ['', 'string']; - testAllArguments(V.isArrayOf, args); - testValidation(SUCCESS, [false], V.isArrayOf, [], 'boolean'); - testValidation(SUCCESS, [false], V.isArrayOf, '', 'boolean'); - testValidation(SUCCESS, { a: [false] }, V.isArrayOf, 'a', 'boolean'); - testValidation(FAILURE, { a: [false, 'true'] }, V.isArrayOf, 'a', 'boolean'); - testValidation([THROW, FAILURE], { a: [] }, V.isArrayOf, 'a', 'unknown_type'); - testValidation([FAILURE], { a: '...' }, V.isArrayOf, 'a', 'boolean'); - testValidation(SUCCESS, { a: [false, 'true'] }, V.isArrayOf, 'a', ['boolean', 'string']); - testValidation(FAILURE, { a: [false, 'true', 0] }, V.isArrayOf, 'a', ['boolean', 'string']); - testValidation([THROW, FAILURE], { a: [] }, V.isArrayOf, 'a', ['unknown_type', 'string']); - testValidation([FAILURE], { a: '...' }, V.isArrayOf, 'a', ['boolean', 'string']); + testAllArguments(V.isArrayOf$, args); + testValidation(SUCCESS, [false], V.isArrayOf$, [], 'boolean'); + testValidation(SUCCESS, [false], V.isArrayOf$, '', 'boolean'); + testValidation(SUCCESS, { a: [false] }, V.isArrayOf$, 'a', 'boolean'); + testValidation(FAILURE, { a: [false, 'true'] }, V.isArrayOf$, 'a', 'boolean'); + testValidation([THROW, FAILURE], { a: [] }, V.isArrayOf$, 'a', 'unknown_type'); + testValidation([FAILURE], { a: '...' }, V.isArrayOf$, 'a', 'boolean'); + testValidation(SUCCESS, { a: [false, 'true'] }, V.isArrayOf$, 'a', ['boolean', 'string']); + testValidation(FAILURE, { a: [false, 'true', 0] }, V.isArrayOf$, 'a', ['boolean', 'string']); + testValidation([THROW, FAILURE], { a: [] }, V.isArrayOf$, 'a', ['unknown_type', 'string']); + testValidation([FAILURE], { a: '...' }, V.isArrayOf$, 'a', ['boolean', 'string']); }); diff --git a/test/util/create-shortcuts.js b/test/util/create-shortcuts.js deleted file mode 100644 index 83f835a..0000000 --- a/test/util/create-shortcuts.js +++ /dev/null @@ -1,48 +0,0 @@ -import { assert } from 'chai'; -import camelcase from 'camelcase'; -import { V, Scope } from '../../src'; -import createShortcuts from '../../src/util/create-shortcuts'; - -describe('Test shortcut opt.', () => { - const F1 = { x: V.isSet, y: V.isType, z: V.every }; - it('Each function xyz should become optXyz', () => { - const target = createShortcuts({}, F1); - assert.deepEqual(Object.keys(F1).map(k => camelcase(`opt ${k}`)), Object.keys(target), ':('); - }); - it('Only function z should become optZ', () => { - const keys = ['z']; - const target = createShortcuts({}, F1, keys); - assert.deepEqual(keys.map(k => camelcase(`opt ${k}`)), Object.keys(target), ':('); - }); - it('Anything other than function should throw an error', () => { - assert.throws(() => createShortcuts({}, { x: 'not a function' }), Error); - }); - it('A function without info should throw an error', () => { - assert.throws(() => createShortcuts({}, { x: () => {} }), Error); - }); - it('A function validator not taking a path as first argument should throw an error', () => { - assert.throws(() => createShortcuts({}, V, ['and']), Error, 'path as first argument'); - }); - it('Missing property at path should always succeed', () => { - const target = createShortcuts({}, V, ['isSet']); - const v = target.optIsSet('a'); - assert(v(new Scope({})) === undefined, ':('); - }); - it('Not missing property at path should always match the original validator', () => { - const target = createShortcuts({}, V, ['isSet']); - const v1 = target.optIsSet('a'); - const v2 = V.isSet('a'); - assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); - }); - it('Missing property at referenced path should always succeed', () => { - const target = createShortcuts({}, V, ['isSet']); - const v = V.def({ p: 'a' }, target.optIsSet({ $var: 'p' })); - assert(v(new Scope({})) === undefined, ':('); - }); - it('Not missing property at referenced path should always match the original validator', () => { - const target = createShortcuts({}, V, ['isSet']); - const v1 = V.def({ p: 'a' }, target.optIsSet({ $var: 'p' })); - const v2 = V.def({ p: 'a' }, V.optIsSet({ $var: 'p' })); - assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); - }); -}); diff --git a/test/util/variants.js b/test/util/variants.js new file mode 100644 index 0000000..0681d8b --- /dev/null +++ b/test/util/variants.js @@ -0,0 +1,58 @@ +import { assert } from 'chai'; +import { V, Scope } from '../../src'; +import { infoVariants, optInfoVariant } from '../../src/util/variants'; + +describe('Test optShortcut().', () => { + function optOf(validator) { + return optInfoVariant(validator).validator; + } + + it('Function xyz should become optXyz', () => { + const optIsSet = optOf(V.isSet); + assert(optIsSet.info.name === 'optIsSet', ':('); + }); + it('Bad value as 1st argument should throw an error at compile time', () => { + const optIsType = optOf(V.isType); + assert.throws(() => optIsType(() => null, 'string'), Error, 'Expected type \'any\''); + }); + it('Bad value as 2nd argument should throw an error at compile time', () => { + const optIsType = optOf(V.isType); + assert.throws(() => optIsType('a', 2), Error, 'Expected type \'string|array\''); + }); + it('Missing property at path should succeed', () => { + const optIsSet = optOf(V.isSet); + const v = optIsSet('a'); + assert(v(new Scope({})) === undefined, ':('); + }); + it('Not missing property at path should always match the original validator', () => { + const optIsSet = optOf(V.isSet); + const v1 = optIsSet('a'); + const v2 = V.isSet('a'); + assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); + }); + it('Missing property at referenced path should always succeed', () => { + const optIsSet = optOf(V.isSet); + const v = V.def({ p: 'a' }, optIsSet({ $var: 'p' })); + assert(v(new Scope({})) === undefined, ':('); + }); + it('Not missing property at referenced path should always match the original validator', () => { + const optIsSet = optOf(V.isSet); + const v1 = V.def({ p: 'a' }, optIsSet({ $var: 'p' })); + const v2 = V.def({ p: 'a' }, V.optIsSet({ $var: 'p' })); + assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); + }); +}); + +describe('Test infoVariants().', () => { + it('Passing a non function should throw an error', () => { + assert.throws(() => infoVariants('not a function'), Error, 'expected a named function'); + }); + it('Passing a non named function should throw an error', () => { + assert.throws(() => infoVariants(() => null), Error, 'expected a named function'); + }); + it('Passing a named function should return the info for that function and its opt shortcut', () => { + function foo(any) { return any; } + const info = infoVariants(foo, 'value:any'); + assert.deepEqual(info.map(i => i.validator.name), ['foo', 'optFoo'], ':('); + }); +}); From 12d06815b05468081b5404bdafabe0d7d6127c41 Mon Sep 17 00:00:00 2001 From: davebaol Date: Fri, 14 Jun 2019 09:38:26 +0200 Subject: [PATCH 06/16] Now info.consolidate() returns this for chaining --- src/leaf-validators/bridge.js | 13 ++++++------- src/util/info.js | 2 +- src/util/variants.js | 11 +++++------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index cff41e5..14626fc 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -42,8 +42,8 @@ class StringOnly extends Bridge { static variants(name, errorFunc, ...argDescriptors) { const adList = ['value:string', ...argDescriptors]; return [ - new StringOnly(name, errorFunc, ...adList), - new StringOnly(`${name}$`, errorFunc, ...adList) + new StringOnly(name, errorFunc, ...adList).consolidate(), + new StringOnly(`${name}$`, errorFunc, ...adList).consolidate() ]; } @@ -99,8 +99,8 @@ class StringOrNumber extends StringOnly { static variants(name, errorFunc, ...argDescriptors) { const adList = ['value:string|number', ...argDescriptors]; return [ - new StringOrNumber(name, errorFunc, ...adList), - new StringOrNumber(`${name}$`, errorFunc, ...adList) + new StringOrNumber(name, errorFunc, ...adList).consolidate(), + new StringOrNumber(`${name}$`, errorFunc, ...adList).consolidate() ]; } @@ -129,8 +129,8 @@ class StringOrArray extends StringOnly { static variants(name, length, type, errorFunc, ...argDescriptors) { const adList = ['value:string|array', ...argDescriptors]; return [ - new StringOrArray(name, length, type, errorFunc, ...adList), - new StringOrArray(`${name}$`, length, type, errorFunc, ...adList) + new StringOrArray(name, length, type, errorFunc, ...adList).consolidate(), + new StringOrArray(`${name}$`, length, type, errorFunc, ...adList).consolidate() ]; } @@ -218,7 +218,6 @@ function bridge(target) { /* eslint-enable no-unused-vars */ vInfo.forEach((info) => { - info.consolidate(); const k = info.name; // 1. Make sure not to overwrite any function already defined in the target // 2. The value from the validator module must be a function (this prevents errors diff --git a/src/util/info.js b/src/util/info.js index 8ea35ed..d47948b 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -96,7 +96,7 @@ class Info { this.validator = this.link(); } this.validator.info = this; - Object.freeze(this); + return Object.freeze(this); // Return this for chaining } } diff --git a/src/util/variants.js b/src/util/variants.js index 6152980..0b0920c 100644 --- a/src/util/variants.js +++ b/src/util/variants.js @@ -23,9 +23,7 @@ function infoVariant(validator, ...argDescriptors) { if (typeof validator !== 'function' || !validator.name) { throw new Error('infoVariant: expected a named function'); } - const info = new Info(validator, ...argDescriptors); - info.consolidate(); - return info; + return new Info(validator, ...argDescriptors).consolidate(); } function optInfoVariant(validator) { @@ -33,9 +31,10 @@ function optInfoVariant(validator) { throw new Error('infoOptVariant: expected a validator whose info property is consolidated'); } const { argDescriptors } = validator.info; - const info = new Info(optShortcut(validator), ...[`${argDescriptors[0].name}:${argDescriptors[0].type.name}?`, ...argDescriptors.slice(1)]); - info.consolidate(); - return info; + return new Info( + optShortcut(validator), + ...[`${argDescriptors[0].name}:${argDescriptors[0].type.name}?`, ...argDescriptors.slice(1)] + ).consolidate(); } function infoVariants(validator, ...argDescriptors) { From b2e1c019bc43a1f395f080efe6fdb294b9c8cdbb Mon Sep 17 00:00:00 2001 From: davebaol Date: Fri, 14 Jun 2019 14:12:16 +0200 Subject: [PATCH 07/16] Variants refactoring --- src/leaf-validators/bridge.js | 170 +++++++++++++++------------------- src/util/variants.js | 64 ++++++++----- test/util/variants.js | 19 ++-- 3 files changed, 128 insertions(+), 125 deletions(-) diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index 14626fc..eb3f92e 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -1,14 +1,7 @@ const v = require('validator'); -const { optInfoVariant } = require('../util/variants'); +const { variants$ } = require('../util/variants'); const Info = require('../util/info'); -class Bridge extends Info { - constructor(name, errorFunc, ...argDescriptors) { - super(name, ...argDescriptors); - this.errorFunc = errorFunc; - } -} - const EMPTY_OBJ = Object.freeze({}); // These functions are optimizations specialized for certain types. @@ -38,13 +31,10 @@ const SPECIALIZED_VALIDATORS = { } }; -class StringOnly extends Bridge { - static variants(name, errorFunc, ...argDescriptors) { - const adList = ['value:string', ...argDescriptors]; - return [ - new StringOnly(name, errorFunc, ...adList).consolidate(), - new StringOnly(`${name}$`, errorFunc, ...adList).consolidate() - ]; +class Bridge extends Info { + constructor(name, errorFunc, firstArgDescriptor, ...otherArgDescriptors) { + super(name, ...[firstArgDescriptor, ...otherArgDescriptors]); + this.errorFunc = errorFunc; } // eslint-disable-next-line class-methods-use-this @@ -95,13 +85,15 @@ class StringOnly extends Bridge { } } -class StringOrNumber extends StringOnly { - static variants(name, errorFunc, ...argDescriptors) { - const adList = ['value:string|number', ...argDescriptors]; - return [ - new StringOrNumber(name, errorFunc, ...adList).consolidate(), - new StringOrNumber(`${name}$`, errorFunc, ...adList).consolidate() - ]; +class StringOnly extends Bridge { + constructor(name, errorFunc, ...argDescriptors) { + super(name, errorFunc, 'value:string', ...argDescriptors); + } +} + +class StringOrNumber extends Bridge { + constructor(name, errorFunc, ...argDescriptors) { + super(name, errorFunc, 'value:string|number', ...argDescriptors); } // eslint-disable-next-line class-methods-use-this @@ -119,21 +111,13 @@ class StringOrNumber extends StringOnly { } } -class StringOrArray extends StringOnly { +class StringOrArray extends Bridge { constructor(name, length, type, errorFunc, ...argDescriptors) { - super(name, errorFunc, ...argDescriptors); + super(name, errorFunc, 'value:string|array', ...argDescriptors); this.length = length; this.type = type; } - static variants(name, length, type, errorFunc, ...argDescriptors) { - const adList = ['value:string|array', ...argDescriptors]; - return [ - new StringOrArray(name, length, type, errorFunc, ...adList).consolidate(), - new StringOrArray(`${name}$`, length, type, errorFunc, ...adList).consolidate() - ]; - } - isSpecializedFor(value) { return Array.isArray(value) && (this.length === undefined || value.length === this.length) @@ -155,65 +139,65 @@ function bridge(target) { /* eslint-disable no-unused-vars */ /* istanbul ignore next */ const vInfo = [ - ...StringOnly.variants('contains', args => `containing the value '${args[0]}'`, 'seed:string'), - // ...StringOnly.variants('equals', args => `equal to the value '${args[0]}'`), - // ...StringOnly.variants('isAfter', args => `equal to the value '${args[0]}'`), - ...StringOnly.variants('isAlpha', args => 'containing only letters (a-zA-Z)', 'locale:string?'), - ...StringOnly.variants('isAlphanumeric', args => 'containing only letters and numbers', 'locale:string?'), - ...StringOnly.variants('isAscii', args => 'containing ASCII chars only'), - ...StringOnly.variants('isBase64', args => 'base64 encoded'), - // ...StringOnly.variants('isBefore', args => `equal to the value '${args[0]}'`), - // ...StringOnly.variants('isBoolean', args => `equal to the value '${args[0]}'`), - ...StringOnly.variants('isByteLength', args => 'whose length (in UTF-8 bytes) falls in the specified range', 'options:object?'), - ...StringOnly.variants('isCreditCard', args => 'representing a credit card'), - ...StringOnly.variants('isCurrency', args => 'representing a valid currency amount', 'options:object?'), - ...StringOnly.variants('isDataURI', args => 'in data uri format'), - // ...StringOnly.variants('isDecimal', args => `equal to the value '${args[0]}'`), - ...StringOrNumber.variants('isDivisibleBy', args => `that's divisible by ${args[0]}`, 'divisor:integer'), - ...StringOnly.variants('isEmail', args => 'representing an email address', 'options:object?'), - ...StringOnly.variants('isEmpty', args => 'having a length of zero', 'options:object?'), - ...StringOrNumber.variants('isFloat', args => 'that\'s a float falling in the specified range', 'options:object?'), - ...StringOnly.variants('isFQDN', args => 'representing a fully qualified domain name (e.g. domain.com)', 'options:object?'), - ...StringOnly.variants('isFullWidth', args => 'containing any full-width chars'), - ...StringOnly.variants('isHalfWidth', args => 'containing any half-width chars'), - ...StringOnly.variants('isHash', args => `matching to the format of the hash algorithm ${args[0]}`, 'algorithm:string?'), - ...StringOnly.variants('isHexadecimal', args => 'representing a hexadecimal number'), - ...StringOnly.variants('isHexColor', args => 'matching to a hexadecimal color'), - ...StringOnly.variants('isIdentityCard', args => 'matching to a valid identity card code', 'locale:string?'), - // ...StringOnly.variants('isIn', args => `equal to the value '${args[0]}'`), - ...StringOrNumber.variants('isInt', args => 'that\'s an integer falling in the specified range', 'options:object?'), - ...StringOnly.variants('isIP', args => 'matching to an IP', 'version:integer?'), - ...StringOnly.variants('isIPRange', args => 'matching to an IP Range'), - ...StringOnly.variants('isISBN', args => 'matching to an ISBN', 'version:integer?'), - ...StringOnly.variants('isISIN', args => 'matching to an ISIN'), - ...StringOnly.variants('isISO31661Alpha2', args => 'matching to a valid ISO 3166-1 alpha-2 officially assigned country code'), - ...StringOnly.variants('isISO31661Alpha3', args => 'matching to a valid ISO 3166-1 alpha-3 officially assigned country code'), - ...StringOnly.variants('isISO8601', args => 'matching to a valid ISO 8601 date'), - ...StringOnly.variants('isISRC', args => 'matching to an ISRC'), - ...StringOnly.variants('isISSN', args => 'matching to an ISSN', 'options:object?'), - ...StringOnly.variants('isJSON', args => 'matching to a valid JSON'), - ...StringOnly.variants('isJWT', args => 'matching to a valid JWT token'), - ...StringOrArray.variants('isLatLong', 2, 'number', args => "representing a valid latitude-longitude coordinate in the format 'lat,long' or 'lat, long'"), - // ...StringOnly.variants('isLength', args => 'whose length falls in the specified range'), - ...StringOnly.variants('isLowercase', args => 'in lowercase'), - ...StringOnly.variants('isMACAddress', args => 'in MAC address format'), - ...StringOnly.variants('isMagnetURI', args => 'in magnet uri format'), - ...StringOnly.variants('isMD5', args => 'representing a valid MD5 hash'), - ...StringOnly.variants('isMimeType', args => 'matching to a valid MIME type format'), - ...StringOnly.variants('isMobilePhone', args => 'representing a mobile phone number', 'locale:string|array?', 'options:object?'), - ...StringOnly.variants('isMongoId', args => 'in the form of a valid hex-encoded representation of a MongoDB ObjectId.'), - ...StringOnly.variants('isMultibyte', args => 'containing one or more multibyte chars'), - ...StringOnly.variants('isNumeric', args => 'containing only numbers', 'options:object?'), - ...StringOrNumber.variants('isPort', args => 'representing a valid port'), - ...StringOnly.variants('isPostalCode', args => 'representing a postal code', 'options:object'), - ...StringOnly.variants('isRFC3339', args => 'matching to a valid RFC 3339 date'), - ...StringOnly.variants('isSurrogatePair', args => 'containing any surrogate pairs chars'), - ...StringOnly.variants('isUppercase', args => 'in uppercase'), - ...StringOnly.variants('isURL', args => 'representing a valid URL', 'options:object?'), - ...StringOnly.variants('isUUID', args => 'matching to a UUID (version 3, 4 or 5)', 'version:integer?'), - ...StringOnly.variants('isVariableWidth', args => 'containing a mixture of full and half-width chars'), - ...StringOnly.variants('isWhitelisted', args => 'whose characters belongs to the whitelist', 'chars:string'), - ...StringOnly.variants('matches', args => `matching the regex '${args[0]}'`, 'pattern:string|regex', 'modifiers:string?') + ...variants$(StringOnly, 'contains', args => `containing the value '${args[0]}'`, 'seed:string'), + // ...variants$(StringOnly, 'equals', args => `equal to the value '${args[0]}'`), + // ...variants$(StringOnly, 'isAfter', args => `equal to the value '${args[0]}'`), + ...variants$(StringOnly, 'isAlpha', args => 'containing only letters (a-zA-Z)', 'locale:string?'), + ...variants$(StringOnly, 'isAlphanumeric', args => 'containing only letters and numbers', 'locale:string?'), + ...variants$(StringOnly, 'isAscii', args => 'containing ASCII chars only'), + ...variants$(StringOnly, 'isBase64', args => 'base64 encoded'), + // ...variants$(StringOnly, 'isBefore', args => `equal to the value '${args[0]}'`), + // ...variants$(StringOnly, 'isBoolean', args => `equal to the value '${args[0]}'`), + ...variants$(StringOnly, 'isByteLength', args => 'whose length (in UTF-8 bytes) falls in the specified range', 'options:object?'), + ...variants$(StringOnly, 'isCreditCard', args => 'representing a credit card'), + ...variants$(StringOnly, 'isCurrency', args => 'representing a valid currency amount', 'options:object?'), + ...variants$(StringOnly, 'isDataURI', args => 'in data uri format'), + // ...variants$(StringOnly, 'isDecimal', args => `equal to the value '${args[0]}'`), + ...variants$(StringOrNumber, 'isDivisibleBy', args => `that's divisible by ${args[0]}`, 'divisor:integer'), + ...variants$(StringOnly, 'isEmail', args => 'representing an email address', 'options:object?'), + ...variants$(StringOnly, 'isEmpty', args => 'having a length of zero', 'options:object?'), + ...variants$(StringOrNumber, 'isFloat', args => 'that\'s a float falling in the specified range', 'options:object?'), + ...variants$(StringOnly, 'isFQDN', args => 'representing a fully qualified domain name (e.g. domain.com)', 'options:object?'), + ...variants$(StringOnly, 'isFullWidth', args => 'containing any full-width chars'), + ...variants$(StringOnly, 'isHalfWidth', args => 'containing any half-width chars'), + ...variants$(StringOnly, 'isHash', args => `matching to the format of the hash algorithm ${args[0]}`, 'algorithm:string?'), + ...variants$(StringOnly, 'isHexadecimal', args => 'representing a hexadecimal number'), + ...variants$(StringOnly, 'isHexColor', args => 'matching to a hexadecimal color'), + ...variants$(StringOnly, 'isIdentityCard', args => 'matching to a valid identity card code', 'locale:string?'), + // ...variants$(StringOnly, 'isIn', args => `equal to the value '${args[0]}'`), + ...variants$(StringOrNumber, 'isInt', args => 'that\'s an integer falling in the specified range', 'options:object?'), + ...variants$(StringOnly, 'isIP', args => 'matching to an IP', 'version:integer?'), + ...variants$(StringOnly, 'isIPRange', args => 'matching to an IP Range'), + ...variants$(StringOnly, 'isISBN', args => 'matching to an ISBN', 'version:integer?'), + ...variants$(StringOnly, 'isISIN', args => 'matching to an ISIN'), + ...variants$(StringOnly, 'isISO31661Alpha2', args => 'matching to a valid ISO 3166-1 alpha-2 officially assigned country code'), + ...variants$(StringOnly, 'isISO31661Alpha3', args => 'matching to a valid ISO 3166-1 alpha-3 officially assigned country code'), + ...variants$(StringOnly, 'isISO8601', args => 'matching to a valid ISO 8601 date'), + ...variants$(StringOnly, 'isISRC', args => 'matching to an ISRC'), + ...variants$(StringOnly, 'isISSN', args => 'matching to an ISSN', 'options:object?'), + ...variants$(StringOnly, 'isJSON', args => 'matching to a valid JSON'), + ...variants$(StringOnly, 'isJWT', args => 'matching to a valid JWT token'), + ...variants$(StringOrArray, 'isLatLong', 2, 'number', args => "representing a valid latitude-longitude coordinate in the format 'lat,long' or 'lat, long'"), + // ...variants$(StringOnly, 'isLength', args => 'whose length falls in the specified range'), + ...variants$(StringOnly, 'isLowercase', args => 'in lowercase'), + ...variants$(StringOnly, 'isMACAddress', args => 'in MAC address format'), + ...variants$(StringOnly, 'isMagnetURI', args => 'in magnet uri format'), + ...variants$(StringOnly, 'isMD5', args => 'representing a valid MD5 hash'), + ...variants$(StringOnly, 'isMimeType', args => 'matching to a valid MIME type format'), + ...variants$(StringOnly, 'isMobilePhone', args => 'representing a mobile phone number', 'locale:string|array?', 'options:object?'), + ...variants$(StringOnly, 'isMongoId', args => 'in the form of a valid hex-encoded representation of a MongoDB ObjectId.'), + ...variants$(StringOnly, 'isMultibyte', args => 'containing one or more multibyte chars'), + ...variants$(StringOnly, 'isNumeric', args => 'containing only numbers', 'options:object?'), + ...variants$(StringOrNumber, 'isPort', args => 'representing a valid port'), + ...variants$(StringOnly, 'isPostalCode', args => 'representing a postal code', 'options:object'), + ...variants$(StringOnly, 'isRFC3339', args => 'matching to a valid RFC 3339 date'), + ...variants$(StringOnly, 'isSurrogatePair', args => 'containing any surrogate pairs chars'), + ...variants$(StringOnly, 'isUppercase', args => 'in uppercase'), + ...variants$(StringOnly, 'isURL', args => 'representing a valid URL', 'options:object?'), + ...variants$(StringOnly, 'isUUID', args => 'matching to a UUID (version 3, 4 or 5)', 'version:integer?'), + ...variants$(StringOnly, 'isVariableWidth', args => 'containing a mixture of full and half-width chars'), + ...variants$(StringOnly, 'isWhitelisted', args => 'whose characters belongs to the whitelist', 'chars:string'), + ...variants$(StringOnly, 'matches', args => `matching the regex '${args[0]}'`, 'pattern:string|regex', 'modifiers:string?') ]; /* eslint-enable no-unused-vars */ @@ -224,10 +208,6 @@ function bridge(target) { // due to changes in new versions of the module) if (!(k in target) && typeof v[info.baseName] === 'function') { target[k] = info.validator; // eslint-disable-line no-param-reassign - - // Add opt variant - const optInfo = optInfoVariant(info.validator); - target[optInfo.name] = optInfo.validator; // eslint-disable-line no-param-reassign } }); return target; diff --git a/src/util/variants.js b/src/util/variants.js index 0b0920c..d268800 100644 --- a/src/util/variants.js +++ b/src/util/variants.js @@ -19,51 +19,71 @@ function optShortcut(validator) { return setFunctionName(optValidator, camelCase(`opt ${validator.info.name}`)); } -function infoVariant(validator, ...argDescriptors) { - if (typeof validator !== 'function' || !validator.name) { - throw new Error('infoVariant: expected a named function'); - } - return new Info(validator, ...argDescriptors).consolidate(); +function variant(InfoClass, validator, ...args) { + return new InfoClass(validator, ...args).consolidate(); } -function optInfoVariant(validator) { - if (typeof validator !== 'function' || !validator.info || !Object.isFrozen(validator.info)) { - throw new Error('infoOptVariant: expected a validator whose info property is consolidated'); - } +function infoVariant(validator, ...args) { + return variant(Info, validator, ...args); +} + +function variantOpt(validator) { const { argDescriptors } = validator.info; - return new Info( + return infoVariant( optShortcut(validator), ...[`${argDescriptors[0].name}:${argDescriptors[0].type.name}?`, ...argDescriptors.slice(1)] - ).consolidate(); + ); } -function infoVariants(validator, ...argDescriptors) { +function variants(InfoClass, validator, ...args) { + const mainVariant = variant(InfoClass, validator, ...args); return [ - infoVariant(validator, ...argDescriptors), - optInfoVariant(validator) + mainVariant, + variantOpt(mainVariant.validator) ]; } +function infoVariants(validator, ...args) { + return variants(Info, validator, ...args); +} + function getVariant(name, commonImpl) { const f = (...args) => commonImpl(f.info, ...args); return setFunctionName(f, name); } -function infoVariants$(commonImpl, ...argDescriptors) { - const { name } = commonImpl; - const func = typeof commonImpl === 'function' ? commonImpl : commonImpl.func; - const validator = getVariant(name, func); - const validator$ = getVariant(`${name}$`, func); +function variants$(InfoClass, commonImpl, ...args) { + let validator; + let validator$; + if (typeof commonImpl === 'function') { + if (!commonImpl.name) { + throw new Error('Expected non anonymous function; otherwise make sure it\'s not an issue due to minification'); + } + validator = getVariant(commonImpl.name, commonImpl); + validator$ = getVariant(`${commonImpl.name}$`, commonImpl); + } else if (typeof commonImpl === 'string') { + validator = commonImpl; + validator$ = `${commonImpl}$`; + } else { + throw new Error('Expected either a named function or its name as first argument'); + } return [ - ...infoVariants(validator, ...argDescriptors), - ...infoVariants(validator$, ...argDescriptors) + ...variants(InfoClass, validator, ...args), + ...variants(InfoClass, validator$, ...args) ]; } +function infoVariants$(commonImpl, ...args) { + return variants$(Info, commonImpl, ...args); +} + module.exports = { + variant, + variants, + variants$, infoVariant, infoVariants, infoVariants$, - optInfoVariant, + variantOpt, optShortcut }; diff --git a/test/util/variants.js b/test/util/variants.js index 0681d8b..e264c1b 100644 --- a/test/util/variants.js +++ b/test/util/variants.js @@ -1,10 +1,10 @@ import { assert } from 'chai'; import { V, Scope } from '../../src'; -import { infoVariants, optInfoVariant } from '../../src/util/variants'; +import { infoVariants, infoVariants$, variantOpt } from '../../src/util/variants'; describe('Test optShortcut().', () => { function optOf(validator) { - return optInfoVariant(validator).validator; + return variantOpt(validator).validator; } it('Function xyz should become optXyz', () => { @@ -44,15 +44,18 @@ describe('Test optShortcut().', () => { }); describe('Test infoVariants().', () => { - it('Passing a non function should throw an error', () => { - assert.throws(() => infoVariants('not a function'), Error, 'expected a named function'); - }); - it('Passing a non named function should throw an error', () => { - assert.throws(() => infoVariants(() => null), Error, 'expected a named function'); - }); it('Passing a named function should return the info for that function and its opt shortcut', () => { function foo(any) { return any; } const info = infoVariants(foo, 'value:any'); assert.deepEqual(info.map(i => i.validator.name), ['foo', 'optFoo'], ':('); }); }); + +describe('Test infoVariants$().', () => { + it('Passing something other than a named function or its name should throw an error', () => { + assert.throws(() => infoVariants$(123), Error, 'Expected either a named function or its name as first argument'); + }); + it('Passing a non named function should throw an error', () => { + assert.throws(() => infoVariants$(() => null), Error, 'Expected non anonymous function'); + }); +}); From f6e057b0ed60920659e7fff677c2d276b9109897 Mon Sep 17 00:00:00 2001 From: davebaol Date: Fri, 14 Jun 2019 15:42:08 +0200 Subject: [PATCH 08/16] Use destructuring assignment --- src/branch-validators.js | 106 +++++++++++++++++------------------ src/leaf-validators/index.js | 60 ++++++++++---------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/branch-validators.js b/src/branch-validators.js index d9c7435..ebab445 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -7,16 +7,16 @@ const { infoVariant, infoVariants$ } = require('./util/variants'); // function call(info, arg, child) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const cExpr = infoArgs[1].compile(child); + const [aArg, cArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } scope.context.push$(info.getValue(aExpr, scope)); @@ -27,9 +27,9 @@ function call(info, arg, child) { } function def(resources, child) { - const infoArgs = def.info.argDescriptors; + const cArg = def.info.argDescriptors[1]; const childScope = Scope.compile(undefined, resources); // Parent scope unknown at compile time - const cExpr = infoArgs[1].compile(child); + const cExpr = cArg.compile(child); return (scope) => { if (!childScope.parent) { childScope.setParent(scope); @@ -38,7 +38,7 @@ function def(resources, child) { try { childScope.resolve(); } catch (e) { return e.message; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, childScope); + cArg.resolve(cExpr, childScope); if (cExpr.error) { return cExpr.error; } } return cExpr.result(childScope); @@ -46,11 +46,11 @@ function def(resources, child) { } function not(child) { - const infoArgs = not.info.argDescriptors; - const cExpr = infoArgs[0].compile(child); + const [cArg] = not.info.argDescriptors; + const cExpr = cArg.compile(child); return (scope) => { if (!cExpr.resolved) { - infoArgs[0].resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } return cExpr.result(scope) ? undefined : 'not: the child validator must fail'; @@ -59,13 +59,13 @@ function not(child) { function and(...children) { const { info } = and; - const childArg = info.argDescriptors[0]; + const [cArg] = info.argDescriptors; const offspring = info.compileRestParams(children); return (scope) => { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - childArg.resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const error = cExpr.result(scope); // Validate child @@ -77,14 +77,14 @@ function and(...children) { function or(...children) { const { info } = or; - const childArg = info.argDescriptors[0]; + const [cArg] = info.argDescriptors; const offspring = info.compileRestParams(children); return (scope) => { let error; for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - childArg.resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } error = cExpr.result(scope); // Validate child @@ -96,14 +96,14 @@ function or(...children) { function xor(...children) { const { info } = xor; - const childArg = info.argDescriptors[0]; + const [cArg] = info.argDescriptors; const offspring = info.compileRestParams(children); return (scope) => { let count = 0; for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - childArg.resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const error = cExpr.result(scope); // Validate child @@ -116,21 +116,21 @@ function xor(...children) { // eslint-disable-next-line no-underscore-dangle function _if(condChild, thenChild, elseChild) { - const infoArgs = _if.info.argDescriptors; - const ccExpr = infoArgs[0].compile(condChild); - const tcExpr = infoArgs[1].compile(thenChild); - const ecExpr = infoArgs[2].compile(elseChild); + const [ccArg, tcArg, ecArg] = _if.info.argDescriptors; + const ccExpr = ccArg.compile(condChild); + const tcExpr = tcArg.compile(thenChild); + const ecExpr = ecArg.compile(elseChild); return (scope) => { if (!ccExpr.resolved) { - infoArgs[0].resolve(ccExpr, scope); + ccArg.resolve(ccExpr, scope); if (ccExpr.error) { return ccExpr.error; } } if (!tcExpr.resolved) { - infoArgs[1].resolve(tcExpr, scope); + tcArg.resolve(tcExpr, scope); if (tcExpr.error) { return tcExpr.error; } } if (!ecExpr.resolved) { - infoArgs[2].resolve(ecExpr, scope); + ecArg.resolve(ecExpr, scope); if (ecExpr.error) { return ecExpr.error; } } if (ecExpr.result == null) { @@ -142,16 +142,16 @@ function _if(condChild, thenChild, elseChild) { } function every(info, arg, child) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const cExpr = infoArgs[1].compile(child); + const [aArg, cArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const $ = scope.find('$'); @@ -202,16 +202,16 @@ function every(info, arg, child) { } function some(info, arg, child) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const cExpr = infoArgs[1].compile(child); + const [aArg, cArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const $ = scope.find('$'); @@ -262,21 +262,21 @@ function some(info, arg, child) { } function alter(resultOnSuccess, resultOnError, child) { - const infoArgs = alter.info.argDescriptors; - const sExpr = infoArgs[0].compile(resultOnSuccess); - const fExpr = infoArgs[1].compile(resultOnError); - const cExpr = infoArgs[2].compile(child); + const [sArg, fArg, cArg] = alter.info.argDescriptors; + const sExpr = sArg.compile(resultOnSuccess); + const fExpr = fArg.compile(resultOnError); + const cExpr = cArg.compile(child); return (scope) => { if (!sExpr.resolved) { - infoArgs[0].resolve(sExpr, scope); + sArg.resolve(sExpr, scope); if (sExpr.error) { return sExpr.error; } } if (!fExpr.resolved) { - infoArgs[1].resolve(fExpr, scope); + fArg.resolve(fExpr, scope); if (fExpr.error) { return fExpr.error; } } if (!cExpr.resolved) { - infoArgs[2].resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } const r = cExpr.result(scope) === undefined ? sExpr.result : fExpr.result; @@ -285,16 +285,16 @@ function alter(resultOnSuccess, resultOnError, child) { } function onError(result, child) { - const infoArgs = onError.info.argDescriptors; - const rExpr = infoArgs[0].compile(result); - const cExpr = infoArgs[1].compile(child); + const [rArg, cArg] = onError.info.argDescriptors; + const rExpr = rArg.compile(result); + const cExpr = cArg.compile(child); return (scope) => { if (!rExpr.resolved) { - infoArgs[0].resolve(rExpr, scope); + rArg.resolve(rExpr, scope); if (rExpr.error) { return rExpr.error; } } if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope); + cArg.resolve(cExpr, scope); if (cExpr.error) { return cExpr.error; } } if (cExpr.result(scope) === undefined) { return undefined; } @@ -304,21 +304,21 @@ function onError(result, child) { // eslint-disable-next-line no-underscore-dangle function _while(info, arg, condChild, doChild) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const ccExpr = infoArgs[1].compile(condChild); - const dcExpr = infoArgs[2].compile(doChild); + const [aArg, ccArg, dcArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const ccExpr = ccArg.compile(condChild); + const dcExpr = dcArg.compile(doChild); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!ccExpr.resolved) { - infoArgs[1].resolve(ccExpr, scope); + ccArg.resolve(ccExpr, scope); if (ccExpr.error) { return ccExpr.error; } } if (!dcExpr.resolved) { - infoArgs[2].resolve(dcExpr, scope); + dcArg.resolve(dcExpr, scope); if (dcExpr.error) { return dcExpr.error; } } const $ = scope.find('$'); diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 9176ff7..0f3af09 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -12,21 +12,21 @@ const { getType } = require('../util/types'); // function equals(info, arg, other, deep) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const oExpr = infoArgs[1].compile(other); - const dExpr = infoArgs[2].compile(deep); + const [aArg, oArg, dArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const oExpr = oArg.compile(other); + const dExpr = dArg.compile(deep); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!oExpr.resolved) { - infoArgs[1].resolve(oExpr, scope); + oArg.resolve(oExpr, scope); if (oExpr.error) { return oExpr.error; } } if (!dExpr.resolved) { - infoArgs[2].resolve(dExpr, scope); + dArg.resolve(dExpr, scope); if (dExpr.error) { return dExpr.error; } } const value = info.getValue(aExpr, scope); @@ -36,16 +36,16 @@ function equals(info, arg, other, deep) { } function isLength(info, arg, options) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const optsExpr = infoArgs[1].compile(options); + const [aArg, optsArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const optsExpr = optsArg.compile(options); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!optsExpr.resolved) { - infoArgs[1].resolve(optsExpr, scope); + optsArg.resolve(optsExpr, scope); if (optsExpr.error) { return optsExpr.error; } } const opts = optsExpr.result; @@ -60,11 +60,11 @@ function isLength(info, arg, options) { } function isSet(info, arg) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); + const [aArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } return info.getValue(aExpr, scope) != null ? undefined : `${info.name}: the value at path '${arg}' must be set`; @@ -72,19 +72,19 @@ function isSet(info, arg) { } function isType(info, arg, type) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const tExpr = infoArgs[1].compile(type); + const [aArg, tArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const tExpr = tArg.compile(type); if (tExpr.resolved) { tExpr.result = getType(tExpr.result); } return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!tExpr.resolved) { - infoArgs[1].resolve(tExpr, scope); + tArg.resolve(tExpr, scope); if (tExpr.error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } @@ -94,16 +94,16 @@ function isType(info, arg, type) { } function isOneOf(info, arg, values) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const vExpr = infoArgs[1].compile(values); + const [aArg, vArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const vExpr = vArg.compile(values); return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!vExpr.resolved) { - infoArgs[1].resolve(vExpr, scope); + vArg.resolve(vExpr, scope); if (vExpr.error) { return vExpr.error; } } return vExpr.result.includes(info.getValue(aExpr, scope)) ? undefined : `${info.name}: the value at path '${arg}' must be one of ${aExpr.result}`; @@ -111,19 +111,19 @@ function isOneOf(info, arg, values) { } function isArrayOf(info, arg, type) { - const infoArgs = info.argDescriptors; - const aExpr = infoArgs[0].compile(arg); - const tExpr = infoArgs[1].compile(type); + const [aArg, tArg] = info.argDescriptors; + const aExpr = aArg.compile(arg); + const tExpr = tArg.compile(type); if (tExpr.resolved) { tExpr.result = getType(tExpr.result); } return (scope) => { if (!aExpr.resolved) { - infoArgs[0].resolve(aExpr, scope); + aArg.resolve(aExpr, scope); if (aExpr.error) { return aExpr.error; } } if (!tExpr.resolved) { - infoArgs[1].resolve(tExpr, scope); + tArg.resolve(tExpr, scope); if (tExpr.error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } From db505fb1189f20a621fa580acb11721f0b455180 Mon Sep 17 00:00:00 2001 From: davebaol Date: Sat, 15 Jun 2019 22:53:12 +0200 Subject: [PATCH 09/16] Expression resolution code more compact --- src/branch-validators.js | 68 ++++++++++++----------------------- src/leaf-validators/bridge.js | 6 ++-- src/leaf-validators/index.js | 36 +++++++------------ src/util/info.js | 3 +- src/util/variants.js | 7 ++-- 5 files changed, 42 insertions(+), 78 deletions(-) diff --git a/src/branch-validators.js b/src/branch-validators.js index ebab445..f87d9d5 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -12,12 +12,10 @@ function call(info, arg, child) { const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } scope.context.push$(info.getValue(aExpr, scope)); const result = cExpr.result(scope); @@ -27,8 +25,8 @@ function call(info, arg, child) { } function def(resources, child) { - const cArg = def.info.argDescriptors[1]; const childScope = Scope.compile(undefined, resources); // Parent scope unknown at compile time + const cArg = def.info.argDescriptors[1]; const cExpr = cArg.compile(child); return (scope) => { if (!childScope.parent) { @@ -38,8 +36,7 @@ function def(resources, child) { try { childScope.resolve(); } catch (e) { return e.message; } } if (!cExpr.resolved) { - cArg.resolve(cExpr, childScope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, childScope).error) { return cExpr.error; } } return cExpr.result(childScope); }; @@ -50,8 +47,7 @@ function not(child) { const cExpr = cArg.compile(child); return (scope) => { if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } return cExpr.result(scope) ? undefined : 'not: the child validator must fail'; }; @@ -65,8 +61,7 @@ function and(...children) { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } const error = cExpr.result(scope); // Validate child if (error) { return error; } @@ -84,8 +79,7 @@ function or(...children) { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } error = cExpr.result(scope); // Validate child if (!error) { return undefined; } @@ -103,8 +97,7 @@ function xor(...children) { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } const error = cExpr.result(scope); // Validate child count += error ? 0 : 1; @@ -122,16 +115,13 @@ function _if(condChild, thenChild, elseChild) { const ecExpr = ecArg.compile(elseChild); return (scope) => { if (!ccExpr.resolved) { - ccArg.resolve(ccExpr, scope); - if (ccExpr.error) { return ccExpr.error; } + if (ccArg.resolve(ccExpr, scope).error) { return ccExpr.error; } } if (!tcExpr.resolved) { - tcArg.resolve(tcExpr, scope); - if (tcExpr.error) { return tcExpr.error; } + if (tcArg.resolve(tcExpr, scope).error) { return tcExpr.error; } } if (!ecExpr.resolved) { - ecArg.resolve(ecExpr, scope); - if (ecExpr.error) { return ecExpr.error; } + if (ecArg.resolve(ecExpr, scope).error) { return ecExpr.error; } } if (ecExpr.result == null) { return ccExpr.result(scope) ? undefined : tcExpr.result(scope); @@ -147,12 +137,10 @@ function every(info, arg, child) { const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } const $ = scope.find('$'); const value = info.getValue(aExpr, scope); @@ -207,12 +195,10 @@ function some(info, arg, child) { const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } const $ = scope.find('$'); const value = info.getValue(aExpr, scope); @@ -268,16 +254,13 @@ function alter(resultOnSuccess, resultOnError, child) { const cExpr = cArg.compile(child); return (scope) => { if (!sExpr.resolved) { - sArg.resolve(sExpr, scope); - if (sExpr.error) { return sExpr.error; } + if (sArg.resolve(sExpr, scope).error) { return sExpr.error; } } if (!fExpr.resolved) { - fArg.resolve(fExpr, scope); - if (fExpr.error) { return fExpr.error; } + if (fArg.resolve(fExpr, scope).error) { return fExpr.error; } } if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } const r = cExpr.result(scope) === undefined ? sExpr.result : fExpr.result; return r == null ? undefined : r; @@ -290,12 +273,10 @@ function onError(result, child) { const cExpr = cArg.compile(child); return (scope) => { if (!rExpr.resolved) { - rArg.resolve(rExpr, scope); - if (rExpr.error) { return rExpr.error; } + if (rArg.resolve(rExpr, scope).error) { return rExpr.error; } } if (!cExpr.resolved) { - cArg.resolve(cExpr, scope); - if (cExpr.error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } } if (cExpr.result(scope) === undefined) { return undefined; } return rExpr.result == null ? undefined : rExpr.result; @@ -310,16 +291,13 @@ function _while(info, arg, condChild, doChild) { const dcExpr = dcArg.compile(doChild); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!ccExpr.resolved) { - ccArg.resolve(ccExpr, scope); - if (ccExpr.error) { return ccExpr.error; } + if (ccArg.resolve(ccExpr, scope).error) { return ccExpr.error; } } if (!dcExpr.resolved) { - dcArg.resolve(dcExpr, scope); - if (dcExpr.error) { return dcExpr.error; } + if (dcArg.resolve(dcExpr, scope).error) { return dcExpr.error; } } const $ = scope.find('$'); const value = info.getValue(aExpr, scope); diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index eb3f92e..1f183d5 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -55,13 +55,13 @@ class Bridge extends Info { const original = v[this.baseName]; const specialized = SPECIALIZED_VALIDATORS[this.baseName]; return (arg, ...restArgs) => { - const aExpr = this.argDescriptors[0].compile(arg); + const aArg = this.argDescriptors[0]; + const aExpr = aArg.compile(arg); const restExpr = this.compileRestParams(restArgs, 1); const restValue = []; return (scope) => { if (!aExpr.resolved) { - this.argDescriptors[0].resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } const errorAt = this.resolveRestParams(restExpr, 1, scope); if (errorAt >= 0) { return restExpr[errorAt].error; } diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 0f3af09..916ae2e 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -18,16 +18,13 @@ function equals(info, arg, other, deep) { const dExpr = dArg.compile(deep); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!oExpr.resolved) { - oArg.resolve(oExpr, scope); - if (oExpr.error) { return oExpr.error; } + if (oArg.resolve(oExpr, scope).error) { return oExpr.error; } } if (!dExpr.resolved) { - dArg.resolve(dExpr, scope); - if (dExpr.error) { return dExpr.error; } + if (dArg.resolve(dExpr, scope).error) { return dExpr.error; } } const value = info.getValue(aExpr, scope); const result = dExpr.result ? deepEqual(value, oExpr.result) : value === oExpr.result; @@ -41,12 +38,10 @@ function isLength(info, arg, options) { const optsExpr = optsArg.compile(options); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!optsExpr.resolved) { - optsArg.resolve(optsExpr, scope); - if (optsExpr.error) { return optsExpr.error; } + if (optsArg.resolve(optsExpr, scope).error) { return optsExpr.error; } } const opts = optsExpr.result; const min = opts.min || 0; @@ -64,8 +59,7 @@ function isSet(info, arg) { const aExpr = aArg.compile(arg); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } return info.getValue(aExpr, scope) != null ? undefined : `${info.name}: the value at path '${arg}' must be set`; }; @@ -80,12 +74,10 @@ function isType(info, arg, type) { } return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!tExpr.resolved) { - tArg.resolve(tExpr, scope); - if (tExpr.error) { return tExpr.error; } + if (tArg.resolve(tExpr, scope).error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } const t = tExpr.result; @@ -99,12 +91,10 @@ function isOneOf(info, arg, values) { const vExpr = vArg.compile(values); return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!vExpr.resolved) { - vArg.resolve(vExpr, scope); - if (vExpr.error) { return vExpr.error; } + if (vArg.resolve(vExpr, scope).error) { return vExpr.error; } } return vExpr.result.includes(info.getValue(aExpr, scope)) ? undefined : `${info.name}: the value at path '${arg}' must be one of ${aExpr.result}`; }; @@ -119,12 +109,10 @@ function isArrayOf(info, arg, type) { } return (scope) => { if (!aExpr.resolved) { - aArg.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } if (!tExpr.resolved) { - tArg.resolve(tExpr, scope); - if (tExpr.error) { return tExpr.error; } + if (tArg.resolve(tExpr, scope).error) { return tExpr.error; } try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } } const value = info.getValue(aExpr, scope); diff --git a/src/util/info.js b/src/util/info.js index d47948b..1633b42 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -43,8 +43,7 @@ class Info { for (let i = 0, len = exprs.length; i < len; i += 1) { if (!exprs[i].resolved) { const ad = this.argDescriptors[this.adjustArgDescriptorIndex(i + offset)]; - ad.resolve(exprs[i], scope); - if (exprs[i].error) { + if (ad.resolve(exprs[i], scope).error) { return i; } } diff --git a/src/util/variants.js b/src/util/variants.js index d268800..de589c2 100644 --- a/src/util/variants.js +++ b/src/util/variants.js @@ -5,13 +5,12 @@ const { setFunctionName } = require('./misc'); function optShortcut(validator) { const optValidator = (arg, ...args) => { const { info } = optValidator; - const argDescriptor0 = info.argDescriptors[0]; - const aExpr = argDescriptor0.compile(arg); + const aArg = info.argDescriptors[0]; + const aExpr = aArg.compile(arg); info.compileRestParams(args, 1); // Make sure other arguments compile correctly return (scope) => { if (!aExpr.resolved) { - argDescriptor0.resolve(aExpr, scope); - if (aExpr.error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } } return (info.getValue(aExpr, scope) ? validator(aExpr.result, ...args)(scope) : undefined); }; From ae0f412da49e6b6503514f6c17bd824bf26872d4 Mon Sep 17 00:00:00 2001 From: davebaol Date: Mon, 17 Jun 2019 09:51:57 +0200 Subject: [PATCH 10/16] Prevent $ shadowing in nested scopes --- src/util/scope.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/util/scope.js b/src/util/scope.js index 9a18d7c..ba896ee 100644 --- a/src/util/scope.js +++ b/src/util/scope.js @@ -43,20 +43,23 @@ class Scope { if (kRef) { throw new Error('Root reference not allowed for scopes'); } + if ('$' in resources) { + throw new Error('$ cannot be shadowed'); + } for (const k in resources) { // eslint-disable-line no-restricted-syntax if (hasOwn.call(resources, k)) { const cur = resources[k]; if (typeof cur === 'object' && cur !== null) { - const type = k.startsWith('$') && k.length > 1 ? child : any; - const ref = type.compile(cur); + const type = k.startsWith('$') ? child : any; + const expr = type.compile(cur); // The check (ref.result !== cur) detects both // references and non hard-coded validators // to trigger shallow copy. - if (!ref.resolved || (ref.resolved && ref.result !== cur)) { + if (!expr.resolved || (expr.resolved && expr.result !== cur)) { if (target === resources) { target = Object.assign({}, resources); // lazy shallow copy } - target[k] = ref.resolved ? ref.result : ref; + target[k] = expr.resolved ? expr.result : expr; } } } @@ -77,7 +80,7 @@ class Scope { if (hasOwn.call(compiledResources, k)) { let resource = compiledResources[k]; if (resource instanceof Expression) { - const type = k.startsWith('$') && k.length > 1 ? child : any; + const type = k.startsWith('$') ? child : any; const ref = type.resolve(resource, this); if (ref.error) { throw new Error(ref.error); } resource = ref.result; From 396f8f60a8a7765f35df25ac80c38fe0dbb31354 Mon Sep 17 00:00:00 2001 From: davebaol Date: Thu, 20 Jun 2019 11:10:52 +0200 Subject: [PATCH 11/16] Support variant specific errors --- src/branch-validators.js | 71 +++++++++++++++++++---------------- src/leaf-validators/bridge.js | 13 ++++--- src/leaf-validators/index.js | 48 ++++++++++++----------- src/util/info.js | 13 ++++++- 4 files changed, 83 insertions(+), 62 deletions(-) diff --git a/src/branch-validators.js b/src/branch-validators.js index f87d9d5..6537f65 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -12,10 +12,10 @@ function call(info, arg, child) { const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } scope.context.push$(info.getValue(aExpr, scope)); const result = cExpr.result(scope); @@ -26,30 +26,32 @@ function call(info, arg, child) { function def(resources, child) { const childScope = Scope.compile(undefined, resources); // Parent scope unknown at compile time - const cArg = def.info.argDescriptors[1]; + const { info } = def; + const cArg = info.argDescriptors[1]; const cExpr = cArg.compile(child); return (scope) => { if (!childScope.parent) { childScope.setParent(scope); } if (!childScope.resolved) { // Let's process references - try { childScope.resolve(); } catch (e) { return e.message; } + try { childScope.resolve(); } catch (e) { return info.error(e.message); } } if (!cExpr.resolved) { - if (cArg.resolve(cExpr, childScope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, childScope).error) { return info.error(cExpr.error); } } return cExpr.result(childScope); }; } function not(child) { - const [cArg] = not.info.argDescriptors; + const { info } = not; + const [cArg] = info.argDescriptors; const cExpr = cArg.compile(child); return (scope) => { if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } - return cExpr.result(scope) ? undefined : 'not: the child validator must fail'; + return cExpr.result(scope) ? undefined : info.error('the child validator must fail'); }; } @@ -61,7 +63,7 @@ function and(...children) { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } const error = cExpr.result(scope); // Validate child if (error) { return error; } @@ -79,7 +81,7 @@ function or(...children) { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } error = cExpr.result(scope); // Validate child if (!error) { return undefined; } @@ -97,31 +99,32 @@ function xor(...children) { for (let i = 0, len = offspring.length; i < len; i += 1) { const cExpr = offspring[i]; if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } const error = cExpr.result(scope); // Validate child count += error ? 0 : 1; if (count === 2) { break; } } - return count === 1 ? undefined : `xor: expected exactly 1 valid child; found ${count} instead`; + return count === 1 ? undefined : info.error(`expected exactly 1 valid child; found ${count} instead`); }; } // eslint-disable-next-line no-underscore-dangle function _if(condChild, thenChild, elseChild) { - const [ccArg, tcArg, ecArg] = _if.info.argDescriptors; + const { info } = _if; + const [ccArg, tcArg, ecArg] = info.argDescriptors; const ccExpr = ccArg.compile(condChild); const tcExpr = tcArg.compile(thenChild); const ecExpr = ecArg.compile(elseChild); return (scope) => { if (!ccExpr.resolved) { - if (ccArg.resolve(ccExpr, scope).error) { return ccExpr.error; } + if (ccArg.resolve(ccExpr, scope).error) { return info.error(ccExpr.error); } } if (!tcExpr.resolved) { - if (tcArg.resolve(tcExpr, scope).error) { return tcExpr.error; } + if (tcArg.resolve(tcExpr, scope).error) { return info.error(tcExpr.error); } } if (!ecExpr.resolved) { - if (ecArg.resolve(ecExpr, scope).error) { return ecExpr.error; } + if (ecArg.resolve(ecExpr, scope).error) { return info.error(ecExpr.error); } } if (ecExpr.result == null) { return ccExpr.result(scope) ? undefined : tcExpr.result(scope); @@ -137,10 +140,10 @@ function every(info, arg, child) { const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } const $ = scope.find('$'); const value = info.getValue(aExpr, scope); @@ -185,7 +188,7 @@ function every(info, arg, child) { scope.context.pop$(); return error; } - return `every: the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`; + return info.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`); }; } @@ -195,10 +198,10 @@ function some(info, arg, child) { const cExpr = cArg.compile(child); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } const $ = scope.find('$'); const value = info.getValue(aExpr, scope); @@ -243,24 +246,25 @@ function some(info, arg, child) { scope.context.pop$(); return error; } - return `some: the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}' instead`; + return info.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}' instead`); }; } function alter(resultOnSuccess, resultOnError, child) { - const [sArg, fArg, cArg] = alter.info.argDescriptors; + const { info } = alter; + const [sArg, fArg, cArg] = info.argDescriptors; const sExpr = sArg.compile(resultOnSuccess); const fExpr = fArg.compile(resultOnError); const cExpr = cArg.compile(child); return (scope) => { if (!sExpr.resolved) { - if (sArg.resolve(sExpr, scope).error) { return sExpr.error; } + if (sArg.resolve(sExpr, scope).error) { return info.error(sExpr.error); } } if (!fExpr.resolved) { - if (fArg.resolve(fExpr, scope).error) { return fExpr.error; } + if (fArg.resolve(fExpr, scope).error) { return info.error(fExpr.error); } } if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } const r = cExpr.result(scope) === undefined ? sExpr.result : fExpr.result; return r == null ? undefined : r; @@ -268,15 +272,16 @@ function alter(resultOnSuccess, resultOnError, child) { } function onError(result, child) { - const [rArg, cArg] = onError.info.argDescriptors; + const { info } = onError; + const [rArg, cArg] = info.argDescriptors; const rExpr = rArg.compile(result); const cExpr = cArg.compile(child); return (scope) => { if (!rExpr.resolved) { - if (rArg.resolve(rExpr, scope).error) { return rExpr.error; } + if (rArg.resolve(rExpr, scope).error) { return info.error(rExpr.error); } } if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return cExpr.error; } + if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } } if (cExpr.result(scope) === undefined) { return undefined; } return rExpr.result == null ? undefined : rExpr.result; @@ -291,13 +296,13 @@ function _while(info, arg, condChild, doChild) { const dcExpr = dcArg.compile(doChild); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!ccExpr.resolved) { - if (ccArg.resolve(ccExpr, scope).error) { return ccExpr.error; } + if (ccArg.resolve(ccExpr, scope).error) { return info.error(ccExpr.error); } } if (!dcExpr.resolved) { - if (dcArg.resolve(dcExpr, scope).error) { return dcExpr.error; } + if (dcArg.resolve(dcExpr, scope).error) { return info.error(dcExpr.error); } } const $ = scope.find('$'); const value = info.getValue(aExpr, scope); @@ -350,7 +355,7 @@ function _while(info, arg, condChild, doChild) { scope.context.pop$(); return error; } - return `while: the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`; + return info.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`); }; } diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index 1f183d5..ba932d6 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -47,8 +47,11 @@ class Bridge extends Info { return value; } - error(path, vArgs) { - return `${this.name}: the value at path '${path}' must be a string ${vArgs ? this.errorFunc(vArgs) : ''}`; + genericError(path, vArgs) { + if (this.is$) { + return this.error(`the value at path '${path}' must be a '${this.originalArg0Desc.type.name}' ${vArgs ? this.errorFunc(vArgs) : ''}`); + } + return this.error(`the first argument must be a '${this.argDescriptors[0].type.name}' ${vArgs ? this.errorFunc(vArgs) : ''}`); } link() { @@ -61,10 +64,10 @@ class Bridge extends Info { const restValue = []; return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } } const errorAt = this.resolveRestParams(restExpr, 1, scope); - if (errorAt >= 0) { return restExpr[errorAt].error; } + if (errorAt >= 0) { return this.error(restExpr[errorAt].error); } for (let i = 0, len = restExpr.length; i < len; i += 1) { restValue[i] = restExpr[i].result; } @@ -79,7 +82,7 @@ class Bridge extends Info { } result = original(value, ...restValue); } - return result ? undefined : this.error(arg, restArgs); + return result ? undefined : this.genericError(arg, restArgs); }; }; } diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 916ae2e..56d2209 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -18,17 +18,17 @@ function equals(info, arg, other, deep) { const dExpr = dArg.compile(deep); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!oExpr.resolved) { - if (oArg.resolve(oExpr, scope).error) { return oExpr.error; } + if (oArg.resolve(oExpr, scope).error) { return info.error(oExpr.error); } } if (!dExpr.resolved) { - if (dArg.resolve(dExpr, scope).error) { return dExpr.error; } + if (dArg.resolve(dExpr, scope).error) { return info.error(dExpr.error); } } const value = info.getValue(aExpr, scope); const result = dExpr.result ? deepEqual(value, oExpr.result) : value === oExpr.result; - return result ? undefined : `${info.name}: expected a value equal to ${oExpr.result}`; + return result ? undefined : info.error(`expected a value equal to ${oExpr.result}`); }; } @@ -38,19 +38,19 @@ function isLength(info, arg, options) { const optsExpr = optsArg.compile(options); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!optsExpr.resolved) { - if (optsArg.resolve(optsExpr, scope).error) { return optsExpr.error; } + if (optsArg.resolve(optsExpr, scope).error) { return info.error(optsExpr.error); } } const opts = optsExpr.result; const min = opts.min || 0; const max = opts.max; // eslint-disable-line prefer-destructuring const len = lengthOf(info.getValue(aExpr, scope)); if (len === undefined) { - return `${info.name}: expected a string, an array or an object`; + return info.error('expected a string, an array or an object'); } - return len >= min && (max === undefined || len <= max) ? undefined : `${info.name}: expected string, array or object of length between ${opts.min} and ${opts.max}`; + return len >= min && (max === undefined || len <= max) ? undefined : info.error(`expected string, array or object of length between ${opts.min} and ${opts.max}`); }; } @@ -59,9 +59,9 @@ function isSet(info, arg) { const aExpr = aArg.compile(arg); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } - return info.getValue(aExpr, scope) != null ? undefined : `${info.name}: the value at path '${arg}' must be set`; + return info.getValue(aExpr, scope) != null ? undefined : info.error(`the value at path '${arg}' must be set`); }; } @@ -74,14 +74,16 @@ function isType(info, arg, type) { } return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!tExpr.resolved) { - if (tArg.resolve(tExpr, scope).error) { return tExpr.error; } - try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } + if (tArg.resolve(tExpr, scope).error) { return info.error(tExpr.error); } + try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { + return info.error(e.message); + } } const t = tExpr.result; - return t.check(info.getValue(aExpr, scope)) ? undefined : `${info.name}: the value at path '${arg}' must be a '${t.name}'`; + return t.check(info.getValue(aExpr, scope)) ? undefined : info.error(`the value at path '${arg}' must be a '${t.name}'`); }; } @@ -91,12 +93,12 @@ function isOneOf(info, arg, values) { const vExpr = vArg.compile(values); return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!vExpr.resolved) { - if (vArg.resolve(vExpr, scope).error) { return vExpr.error; } + if (vArg.resolve(vExpr, scope).error) { return info.error(vExpr.error); } } - return vExpr.result.includes(info.getValue(aExpr, scope)) ? undefined : `${info.name}: the value at path '${arg}' must be one of ${aExpr.result}`; + return vExpr.result.includes(info.getValue(aExpr, scope)) ? undefined : info.error(`the value at path '${arg}' must be one of ${aExpr.result}`); }; } @@ -109,17 +111,19 @@ function isArrayOf(info, arg, type) { } return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } if (!tExpr.resolved) { - if (tArg.resolve(tExpr, scope).error) { return tExpr.error; } - try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { return e.message; } + if (tArg.resolve(tExpr, scope).error) { return info.error(tExpr.error); } + try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { + return info.error(e.message); + } } const value = info.getValue(aExpr, scope); const t = tExpr.result; - if (!Array.isArray(value)) return `${info.name}: the value at path '${arg}' must be an array`; + if (!Array.isArray(value)) return info.error(`the value at path '${arg}' must be an array`); const flag = value.every(e => t.check(e)); - return flag ? undefined : `${info.name}: the value at path '${arg}' must be an array of '${t.name}'`; + return flag ? undefined : info.error(`the value at path '${arg}' must be an array of '${t.name}'`); }; } diff --git a/src/util/info.js b/src/util/info.js index 1633b42..ac72090 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -20,14 +20,20 @@ class Info { this.getValue = (expr, scope) => get(scope.find('$'), expr.result); // will be processed in consolidate() this.argDescriptors = ['path:path', ...argDescriptors.slice(1)]; + [this.originalArg0Desc] = argDescriptors; } else { this.baseName = this.name; this.getValue = expr => expr.result; // will be processed in consolidate() this.argDescriptors = argDescriptors; + this.originalArg0Desc = undefined; } } + error(msg) { + return `${this.name}: ${msg}`; + } + compileRestParams(args, offset = 0) { const exprs = []; for (let i = 0; i < args.length; i += 1) { @@ -74,16 +80,19 @@ class Info { try { a = new Argument(d, context); } catch (e) { - throw new Error(`Validator '${this.name}' argument at index ${i}: ${e.message}`); + throw new Error(this.error(`bad argument at index ${i}: ${e.message}`)); } if (i < last && a.restParams) { - throw new Error(`Validator '${this.name}' argument at index ${i}: rest parameter is legal only for the last argument`); + throw new Error(this.error(`bad argument at index ${i}: rest parameter is allowed only for the last argument`)); } if (a.type.acceptsValidator) { this.isLeaf = false; } return a; }); + if (this.originalArg0Desc) { + this.originalArg0Desc = new Argument(this.originalArg0Desc, context); + } } /* From e1f3c259eb38f138d65bf62e52658c5d3ce37593 Mon Sep 17 00:00:00 2001 From: davebaol Date: Thu, 20 Jun 2019 14:37:26 +0200 Subject: [PATCH 12/16] Use info.error in opt variant too --- src/util/variants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/variants.js b/src/util/variants.js index de589c2..cba4771 100644 --- a/src/util/variants.js +++ b/src/util/variants.js @@ -10,7 +10,7 @@ function optShortcut(validator) { info.compileRestParams(args, 1); // Make sure other arguments compile correctly return (scope) => { if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return aExpr.error; } + if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } } return (info.getValue(aExpr, scope) ? validator(aExpr.result, ...args)(scope) : undefined); }; From 0ad469fd4f0f7ebb7fb973a5b9ef4aa090320b92 Mon Sep 17 00:00:00 2001 From: davebaol Date: Sun, 23 Jun 2019 00:59:00 +0200 Subject: [PATCH 13/16] Update babel-eslint and set-value --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e36ec2a..79ed272 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "homepage": "https://github.com/davebaol/hrb-validator#readme", "devDependencies": { "babel-cli": "^6.26.0", - "babel-eslint": "^10.0.1", + "babel-eslint": "^10.0.2", "babel-plugin-add-module-exports": "^1.0.2", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.6.1", @@ -63,7 +63,7 @@ "is-plain-object": "^3.0.0", "is-regexp": "^2.1.0", "rfdc": "^1.1.4", - "set-value": "^3.0.0", + "set-value": "^3.0.1", "validator": "^10.11.0" }, "publishConfig": { From 1e00837eb95f82bca180bfae28e710f79cd127f9 Mon Sep 17 00:00:00 2001 From: davebaol Date: Sun, 23 Jun 2019 01:02:09 +0200 Subject: [PATCH 14/16] Update eslint-plugin-import --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79ed272..f6f2d87 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "cross-env": "^5.1.3", "eslint": "^5.16.0", "eslint-config-airbnb": "^17.1.0", - "eslint-plugin-import": "^2.17.2", + "eslint-plugin-import": "^2.17.3", "eslint-plugin-jsx-a11y": "^6.0.2", "eslint-plugin-react": "^7.13.0", "mocha": "^6.1.4", From 8c3450ac7ff1bbe96e0bd2c24c80d7ec3b532983 Mon Sep 17 00:00:00 2001 From: davebaol Date: Sun, 23 Jun 2019 14:49:57 +0200 Subject: [PATCH 15/16] Variants refactoring --- src/branch-validators.js | 759 +++++++++++++++++++--------------- src/leaf-validators/bridge.js | 131 +++--- src/leaf-validators/index.js | 260 +++++++----- src/util/info.js | 84 ++-- src/util/variants.js | 88 ---- test/util/info.js | 120 ++++-- test/util/scope.js | 4 + test/util/variants.js | 61 --- 8 files changed, 765 insertions(+), 742 deletions(-) delete mode 100644 src/util/variants.js delete mode 100644 test/util/variants.js diff --git a/src/branch-validators.js b/src/branch-validators.js index 6537f65..1290269 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -1,381 +1,456 @@ +const Info = require('./util/info'); const Scope = require('./util/scope'); -const { infoVariant, infoVariants$ } = require('./util/variants'); // // BRANCH VALIDATORS // They all take at least one child validator as arguments. // -function call(info, arg, child) { - const [aArg, cArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const cExpr = cArg.compile(child); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - scope.context.push$(info.getValue(aExpr, scope)); - const result = cExpr.result(scope); - scope.context.pop$(); - return result; - }; +/* eslint-disable lines-between-class-members */ + +class Call extends Info { + constructor() { + super('call', 'value:any', 'child:child'); + } + create() { + return (arg, child) => { + const [aArg, cArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const cExpr = cArg.compile(child); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + scope.context.push$(this.getValue(aExpr, scope)); + const result = cExpr.result(scope); + scope.context.pop$(); + return result; + }; + }; + } } -function def(resources, child) { - const childScope = Scope.compile(undefined, resources); // Parent scope unknown at compile time - const { info } = def; - const cArg = info.argDescriptors[1]; - const cExpr = cArg.compile(child); - return (scope) => { - if (!childScope.parent) { - childScope.setParent(scope); - } - if (!childScope.resolved) { // Let's process references - try { childScope.resolve(); } catch (e) { return info.error(e.message); } - } - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, childScope).error) { return info.error(cExpr.error); } - } - return cExpr.result(childScope); - }; +class Def extends Info { + constructor() { + super('def', { def: 'scope:object', refDepth: -1 }, 'child:child'); + } + create() { + return (resources, child) => { + // Parent scope unknown at compile time + const childScope = Scope.compile(undefined, resources); + const cArg = this.argDescriptors[1]; + const cExpr = cArg.compile(child); + return (scope) => { + if (!childScope.parent) { + childScope.setParent(scope); + } + if (!childScope.resolved) { // Let's process references + try { childScope.resolve(); } catch (e) { return this.error(e.message); } + } + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, childScope).error) { return this.error(cExpr.error); } + } + return cExpr.result(childScope); + }; + }; + } } -function not(child) { - const { info } = not; - const [cArg] = info.argDescriptors; - const cExpr = cArg.compile(child); - return (scope) => { - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - return cExpr.result(scope) ? undefined : info.error('the child validator must fail'); - }; +class Not extends Info { + constructor() { + super('not', 'child:child'); + } + create() { + return (child) => { + const [cArg] = this.argDescriptors; + const cExpr = cArg.compile(child); + return (scope) => { + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + return cExpr.result(scope) ? undefined : this.error('the child validator must fail'); + }; + }; + } } -function and(...children) { - const { info } = and; - const [cArg] = info.argDescriptors; - const offspring = info.compileRestParams(children); - return (scope) => { - for (let i = 0, len = offspring.length; i < len; i += 1) { - const cExpr = offspring[i]; - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - const error = cExpr.result(scope); // Validate child - if (error) { return error; } - } - return undefined; - }; +class And extends Info { + constructor() { + super('and', '...child:child'); + } + create() { + return (...children) => { + const [cArg] = this.argDescriptors; + const offspring = this.compileRestParams(children); + return (scope) => { + for (let i = 0, len = offspring.length; i < len; i += 1) { + const cExpr = offspring[i]; + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + const error = cExpr.result(scope); // Validate child + if (error) { return error; } + } + return undefined; + }; + }; + } } -function or(...children) { - const { info } = or; - const [cArg] = info.argDescriptors; - const offspring = info.compileRestParams(children); - return (scope) => { - let error; - for (let i = 0, len = offspring.length; i < len; i += 1) { - const cExpr = offspring[i]; - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - error = cExpr.result(scope); // Validate child - if (!error) { return undefined; } - } - return error; - }; +class Or extends Info { + constructor() { + super('or', '...child:child'); + } + create() { + return (...children) => { + const [cArg] = this.argDescriptors; + const offspring = this.compileRestParams(children); + return (scope) => { + let error; + for (let i = 0, len = offspring.length; i < len; i += 1) { + const cExpr = offspring[i]; + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + error = cExpr.result(scope); // Validate child + if (!error) { return undefined; } + } + return error; + }; + }; + } } -function xor(...children) { - const { info } = xor; - const [cArg] = info.argDescriptors; - const offspring = info.compileRestParams(children); - return (scope) => { - let count = 0; - for (let i = 0, len = offspring.length; i < len; i += 1) { - const cExpr = offspring[i]; - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - const error = cExpr.result(scope); // Validate child - count += error ? 0 : 1; - if (count === 2) { break; } - } - return count === 1 ? undefined : info.error(`expected exactly 1 valid child; found ${count} instead`); - }; +class Xor extends Info { + constructor() { + super('xor', '...child:child'); + } + create() { + return (...children) => { + const [cArg] = this.argDescriptors; + const offspring = this.compileRestParams(children); + return (scope) => { + let count = 0; + for (let i = 0, len = offspring.length; i < len; i += 1) { + const cExpr = offspring[i]; + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + const error = cExpr.result(scope); // Validate child + count += error ? 0 : 1; + if (count === 2) { break; } + } + return count === 1 ? undefined : this.error(`expected exactly 1 valid child; found ${count} instead`); + }; + }; + } } -// eslint-disable-next-line no-underscore-dangle -function _if(condChild, thenChild, elseChild) { - const { info } = _if; - const [ccArg, tcArg, ecArg] = info.argDescriptors; - const ccExpr = ccArg.compile(condChild); - const tcExpr = tcArg.compile(thenChild); - const ecExpr = ecArg.compile(elseChild); - return (scope) => { - if (!ccExpr.resolved) { - if (ccArg.resolve(ccExpr, scope).error) { return info.error(ccExpr.error); } - } - if (!tcExpr.resolved) { - if (tcArg.resolve(tcExpr, scope).error) { return info.error(tcExpr.error); } - } - if (!ecExpr.resolved) { - if (ecArg.resolve(ecExpr, scope).error) { return info.error(ecExpr.error); } - } - if (ecExpr.result == null) { - return ccExpr.result(scope) ? undefined : tcExpr.result(scope); - } - // Either then or else is validated, never both! - return (ccExpr.result(scope) ? ecExpr.result : tcExpr.result)(scope); - }; +class If extends Info { + constructor() { + super('if', 'cond:child', 'then:child', 'else:child?'); + } + create() { + return (condChild, thenChild, elseChild) => { + const [ccArg, tcArg, ecArg] = this.argDescriptors; + const ccExpr = ccArg.compile(condChild); + const tcExpr = tcArg.compile(thenChild); + const ecExpr = ecArg.compile(elseChild); + return (scope) => { + if (!ccExpr.resolved) { + if (ccArg.resolve(ccExpr, scope).error) { return this.error(ccExpr.error); } + } + if (!tcExpr.resolved) { + if (tcArg.resolve(tcExpr, scope).error) { return this.error(tcExpr.error); } + } + if (!ecExpr.resolved) { + if (ecArg.resolve(ecExpr, scope).error) { return this.error(ecExpr.error); } + } + if (ecExpr.result == null) { + return ccExpr.result(scope) ? undefined : tcExpr.result(scope); + } + // Either then or else is validated, never both! + return (ccExpr.result(scope) ? ecExpr.result : tcExpr.result)(scope); + }; + }; + } } -function every(info, arg, child) { - const [aArg, cArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const cExpr = cArg.compile(child); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - const $ = scope.find('$'); - const value = info.getValue(aExpr, scope); - if (Array.isArray(value)) { - const new$ = { original: $ }; - scope.context.push$(new$); - let error; - const found = value.find((item, index) => { - new$.value = item; - new$.index = index; - error = cExpr.result(scope); - return error; - }); - scope.context.pop$(); - return found ? error : undefined; - } - if (typeof value === 'object') { - const new$ = { original: $ }; - scope.context.push$(new$); - let error; - const found = Object.keys(value).find((key, index) => { - new$.key = key; - new$.value = value[key]; - new$.index = index; - error = cExpr.result(scope); - return error; - }); - scope.context.pop$(); - return found ? error : undefined; - } - if (typeof value === 'string') { - const new$ = { original: $ }; - scope.context.push$(new$); - let error; - // eslint-disable-next-line no-cond-assign - for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { - new$.value = char; - new$.index = index; - error = cExpr.result(scope); - if (error) { break; } - } - scope.context.pop$(); - return error; - } - return info.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`); - }; +class Every extends Info { + constructor() { + super('every', 'value:any', 'child:child'); + } + create() { + return (arg, child) => { + const [aArg, cArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const cExpr = cArg.compile(child); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + const $ = scope.find('$'); + const value = this.getValue(aExpr, scope); + if (Array.isArray(value)) { + const new$ = { original: $ }; + scope.context.push$(new$); + let error; + const found = value.find((item, index) => { + new$.value = item; + new$.index = index; + error = cExpr.result(scope); + return error; + }); + scope.context.pop$(); + return found ? error : undefined; + } + if (typeof value === 'object') { + const new$ = { original: $ }; + scope.context.push$(new$); + let error; + const found = Object.keys(value).find((key, index) => { + new$.key = key; + new$.value = value[key]; + new$.index = index; + error = cExpr.result(scope); + return error; + }); + scope.context.pop$(); + return found ? error : undefined; + } + if (typeof value === 'string') { + const new$ = { original: $ }; + scope.context.push$(new$); + let error; + // eslint-disable-next-line no-cond-assign + for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { + new$.value = char; + new$.index = index; + error = cExpr.result(scope); + if (error) { break; } + } + scope.context.pop$(); + return error; + } + return this.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`); + }; + }; + } } -function some(info, arg, child) { - const [aArg, cArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const cExpr = cArg.compile(child); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - const $ = scope.find('$'); - const value = info.getValue(aExpr, scope); - if (Array.isArray(value)) { - const new$ = { original: $ }; - scope.context.push$(new$); - let error; - const found = value.find((item, index) => { - new$.value = item; - new$.index = index; - error = cExpr.result(scope); - return !error; - }); - scope.context.pop$(); - return found ? undefined : error; - } - if (typeof value === 'object') { - const new$ = { original: $ }; - scope.context.push$(new$); - let error; - const found = Object.keys(value).find((key, index) => { - new$.key = key; - new$.value = value[key]; - new$.index = index; - error = cExpr.result(scope); - return !error; - }); - scope.context.pop$(); - return found ? undefined : error; - } - if (typeof value === 'string') { - const new$ = { original: $ }; - scope.context.push$(new$); - let error; - // eslint-disable-next-line no-cond-assign - for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { - new$.value = char; - new$.index = index; - error = cExpr.result(scope); - if (!error) { break; } - } - scope.context.pop$(); - return error; - } - return info.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}' instead`); - }; +class Some extends Info { + constructor() { + super('some', 'value:any', 'child:child'); + } + create() { + return (arg, child) => { + const [aArg, cArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const cExpr = cArg.compile(child); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + const $ = scope.find('$'); + const value = this.getValue(aExpr, scope); + if (Array.isArray(value)) { + const new$ = { original: $ }; + scope.context.push$(new$); + let error; + const found = value.find((item, index) => { + new$.value = item; + new$.index = index; + error = cExpr.result(scope); + return !error; + }); + scope.context.pop$(); + return found ? undefined : error; + } + if (typeof value === 'object') { + const new$ = { original: $ }; + scope.context.push$(new$); + let error; + const found = Object.keys(value).find((key, index) => { + new$.key = key; + new$.value = value[key]; + new$.index = index; + error = cExpr.result(scope); + return !error; + }); + scope.context.pop$(); + return found ? undefined : error; + } + if (typeof value === 'string') { + const new$ = { original: $ }; + scope.context.push$(new$); + let error; + // eslint-disable-next-line no-cond-assign + for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { + new$.value = char; + new$.index = index; + error = cExpr.result(scope); + if (!error) { break; } + } + scope.context.pop$(); + return error; + } + return this.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}' instead`); + }; + }; + } } -function alter(resultOnSuccess, resultOnError, child) { - const { info } = alter; - const [sArg, fArg, cArg] = info.argDescriptors; - const sExpr = sArg.compile(resultOnSuccess); - const fExpr = fArg.compile(resultOnError); - const cExpr = cArg.compile(child); - return (scope) => { - if (!sExpr.resolved) { - if (sArg.resolve(sExpr, scope).error) { return info.error(sExpr.error); } - } - if (!fExpr.resolved) { - if (fArg.resolve(fExpr, scope).error) { return info.error(fExpr.error); } - } - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - const r = cExpr.result(scope) === undefined ? sExpr.result : fExpr.result; - return r == null ? undefined : r; - }; +class Alter extends Info { + constructor() { + super('alter', 'resultOnSuccess:any?', 'resultOnError:any?', 'child:child'); + } + create() { + return (resultOnSuccess, resultOnError, child) => { + const [sArg, fArg, cArg] = this.argDescriptors; + const sExpr = sArg.compile(resultOnSuccess); + const fExpr = fArg.compile(resultOnError); + const cExpr = cArg.compile(child); + return (scope) => { + if (!sExpr.resolved) { + if (sArg.resolve(sExpr, scope).error) { return this.error(sExpr.error); } + } + if (!fExpr.resolved) { + if (fArg.resolve(fExpr, scope).error) { return this.error(fExpr.error); } + } + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + const r = cExpr.result(scope) === undefined ? sExpr.result : fExpr.result; + return r == null ? undefined : r; + }; + }; + } } -function onError(result, child) { - const { info } = onError; - const [rArg, cArg] = info.argDescriptors; - const rExpr = rArg.compile(result); - const cExpr = cArg.compile(child); - return (scope) => { - if (!rExpr.resolved) { - if (rArg.resolve(rExpr, scope).error) { return info.error(rExpr.error); } - } - if (!cExpr.resolved) { - if (cArg.resolve(cExpr, scope).error) { return info.error(cExpr.error); } - } - if (cExpr.result(scope) === undefined) { return undefined; } - return rExpr.result == null ? undefined : rExpr.result; - }; +class OnError extends Info { + constructor() { + super('onError', 'result:any?', 'child:child'); + } + create() { + return (result, child) => { + const [rArg, cArg] = this.argDescriptors; + const rExpr = rArg.compile(result); + const cExpr = cArg.compile(child); + return (scope) => { + if (!rExpr.resolved) { + if (rArg.resolve(rExpr, scope).error) { return this.error(rExpr.error); } + } + if (!cExpr.resolved) { + if (cArg.resolve(cExpr, scope).error) { return this.error(cExpr.error); } + } + if (cExpr.result(scope) === undefined) { return undefined; } + return rExpr.result == null ? undefined : rExpr.result; + }; + }; + } } -// eslint-disable-next-line no-underscore-dangle -function _while(info, arg, condChild, doChild) { - const [aArg, ccArg, dcArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const ccExpr = ccArg.compile(condChild); - const dcExpr = dcArg.compile(doChild); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!ccExpr.resolved) { - if (ccArg.resolve(ccExpr, scope).error) { return info.error(ccExpr.error); } - } - if (!dcExpr.resolved) { - if (dcArg.resolve(dcExpr, scope).error) { return info.error(dcExpr.error); } - } - const $ = scope.find('$'); - const value = info.getValue(aExpr, scope); - const status = { succeeded: 0, failed: 0, original: $ }; - if (Array.isArray(value)) { - scope.context.push$(status); - let error; - const found = value.find((item, index) => { - status.index = index; - status.value = item; - error = ccExpr.result(scope); - if (!error) { - status.failed += dcExpr.result(scope) ? 1 : 0; - status.succeeded = index + 1 - status.failed; +class While extends Info { + constructor() { + super('while', 'value:any', 'cond:child', 'do:child'); + } + create() { + return (arg, condChild, doChild) => { + const [aArg, ccArg, dcArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const ccExpr = ccArg.compile(condChild); + const dcExpr = dcArg.compile(doChild); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } } - return error; - }); - scope.context.pop$(); - return found ? error : undefined; - } - if (typeof value === 'object') { - scope.context.push$(status); - let error; - const found = Object.keys(value).find((key, index) => { - status.index = index; - status.key = key; - status.value = value[key]; - error = ccExpr.result(scope); - if (!error) { - status.failed += dcExpr.result(scope) ? 1 : 0; - status.succeeded = index + 1 - status.failed; + if (!ccExpr.resolved) { + if (ccArg.resolve(ccExpr, scope).error) { return this.error(ccExpr.error); } } - return error; - }); - scope.context.pop$(); - return found ? error : undefined; - } - if (typeof value === 'string') { - scope.context.push$(status); - let error; - // eslint-disable-next-line no-cond-assign - for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { - status.index = index; - status.value = char; - error = ccExpr.result(scope); - if (error) { break; } - status.failed += dcExpr.result(scope) ? 1 : 0; - status.succeeded = index + 1 - status.failed; - } - scope.context.pop$(); - return error; - } - return info.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`); - }; + if (!dcExpr.resolved) { + if (dcArg.resolve(dcExpr, scope).error) { return this.error(dcExpr.error); } + } + const $ = scope.find('$'); + const value = this.getValue(aExpr, scope); + const status = { succeeded: 0, failed: 0, original: $ }; + if (Array.isArray(value)) { + scope.context.push$(status); + let error; + const found = value.find((item, index) => { + status.index = index; + status.value = item; + error = ccExpr.result(scope); + if (!error) { + status.failed += dcExpr.result(scope) ? 1 : 0; + status.succeeded = index + 1 - status.failed; + } + return error; + }); + scope.context.pop$(); + return found ? error : undefined; + } + if (typeof value === 'object') { + scope.context.push$(status); + let error; + const found = Object.keys(value).find((key, index) => { + status.index = index; + status.key = key; + status.value = value[key]; + error = ccExpr.result(scope); + if (!error) { + status.failed += dcExpr.result(scope) ? 1 : 0; + status.succeeded = index + 1 - status.failed; + } + return error; + }); + scope.context.pop$(); + return found ? error : undefined; + } + if (typeof value === 'string') { + scope.context.push$(status); + let error; + // eslint-disable-next-line no-cond-assign + for (let index = 0, char = ''; (char = value.charAt(index)); index += 1) { + status.index = index; + status.value = char; + error = ccExpr.result(scope); + if (error) { break; } + status.failed += dcExpr.result(scope) ? 1 : 0; + status.succeeded = index + 1 - status.failed; + } + scope.context.pop$(); + return error; + } + return this.error(`the value at path '${arg}' must be either a string, an array or an object; found type '${typeof value}'`); + }; + }; + } } function branchValidators() { - /* eslint-disable no-unused-vars */ const vInfo = [ - ...infoVariants$(call, 'value:any', 'child:child'), - infoVariant(def, { def: 'scope:object', refDepth: -1 }, 'child:child'), - infoVariant(not, 'child:child'), - infoVariant(and, '...child:child'), - infoVariant(or, '...child:child'), - infoVariant(xor, '...child:child'), - infoVariant(_if, 'cond:child', 'then:child', 'else:child?'), - ...infoVariants$(every, 'value:any', 'child:child'), - ...infoVariants$(some, 'value:any', 'child:child'), - infoVariant(alter, 'resultOnSuccess:any?', 'resultOnError:any?', 'child:child'), - infoVariant(onError, 'result:any?', 'child:child'), - ...infoVariants$(_while, 'value:any', 'cond:child', 'do:child') + ...Info.variants(Call), + new Def().prepare(), + new Not().prepare(), + new And().prepare(), + new Or().prepare(), + new Xor().prepare(), + new If().prepare(), + ...Info.variants(Every), + ...Info.variants(Some), + new Alter().prepare(), + new OnError().prepare(), + ...Info.variants(While) ]; - /* eslint-enable no-unused-vars */ const target = vInfo.reduce((acc, info) => { const k = info.name; diff --git a/src/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index ba932d6..f49dd22 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -1,5 +1,4 @@ const v = require('validator'); -const { variants$ } = require('../util/variants'); const Info = require('../util/info'); const EMPTY_OBJ = Object.freeze({}); @@ -54,7 +53,7 @@ class Bridge extends Info { return this.error(`the first argument must be a '${this.argDescriptors[0].type.name}' ${vArgs ? this.errorFunc(vArgs) : ''}`); } - link() { + create() { const original = v[this.baseName]; const specialized = SPECIALIZED_VALIDATORS[this.baseName]; return (arg, ...restArgs) => { @@ -108,10 +107,6 @@ class StringOrNumber extends Bridge { cast(value) { return typeof value === 'number' ? String(value) : value; } - - error(path, vArgs) { - return `${this.name}: the value at path '${path}' must be either a string or a number ${vArgs ? this.errorFunc(vArgs) : ''}`; - } } class StringOrArray extends Bridge { @@ -132,75 +127,71 @@ class StringOrArray extends Bridge { cast(value) { return Array.isArray(value) ? value.join(',') : value; } - - error(path, vArgs) { - return `${this.name}: the value at path '${path}' must be either a string or an array ${vArgs ? this.errorFunc(vArgs) : ''}`; - } } function bridge(target) { - /* eslint-disable no-unused-vars */ + /* eslint-disable no-unused-vars, max-len */ /* istanbul ignore next */ const vInfo = [ - ...variants$(StringOnly, 'contains', args => `containing the value '${args[0]}'`, 'seed:string'), - // ...variants$(StringOnly, 'equals', args => `equal to the value '${args[0]}'`), - // ...variants$(StringOnly, 'isAfter', args => `equal to the value '${args[0]}'`), - ...variants$(StringOnly, 'isAlpha', args => 'containing only letters (a-zA-Z)', 'locale:string?'), - ...variants$(StringOnly, 'isAlphanumeric', args => 'containing only letters and numbers', 'locale:string?'), - ...variants$(StringOnly, 'isAscii', args => 'containing ASCII chars only'), - ...variants$(StringOnly, 'isBase64', args => 'base64 encoded'), - // ...variants$(StringOnly, 'isBefore', args => `equal to the value '${args[0]}'`), - // ...variants$(StringOnly, 'isBoolean', args => `equal to the value '${args[0]}'`), - ...variants$(StringOnly, 'isByteLength', args => 'whose length (in UTF-8 bytes) falls in the specified range', 'options:object?'), - ...variants$(StringOnly, 'isCreditCard', args => 'representing a credit card'), - ...variants$(StringOnly, 'isCurrency', args => 'representing a valid currency amount', 'options:object?'), - ...variants$(StringOnly, 'isDataURI', args => 'in data uri format'), - // ...variants$(StringOnly, 'isDecimal', args => `equal to the value '${args[0]}'`), - ...variants$(StringOrNumber, 'isDivisibleBy', args => `that's divisible by ${args[0]}`, 'divisor:integer'), - ...variants$(StringOnly, 'isEmail', args => 'representing an email address', 'options:object?'), - ...variants$(StringOnly, 'isEmpty', args => 'having a length of zero', 'options:object?'), - ...variants$(StringOrNumber, 'isFloat', args => 'that\'s a float falling in the specified range', 'options:object?'), - ...variants$(StringOnly, 'isFQDN', args => 'representing a fully qualified domain name (e.g. domain.com)', 'options:object?'), - ...variants$(StringOnly, 'isFullWidth', args => 'containing any full-width chars'), - ...variants$(StringOnly, 'isHalfWidth', args => 'containing any half-width chars'), - ...variants$(StringOnly, 'isHash', args => `matching to the format of the hash algorithm ${args[0]}`, 'algorithm:string?'), - ...variants$(StringOnly, 'isHexadecimal', args => 'representing a hexadecimal number'), - ...variants$(StringOnly, 'isHexColor', args => 'matching to a hexadecimal color'), - ...variants$(StringOnly, 'isIdentityCard', args => 'matching to a valid identity card code', 'locale:string?'), - // ...variants$(StringOnly, 'isIn', args => `equal to the value '${args[0]}'`), - ...variants$(StringOrNumber, 'isInt', args => 'that\'s an integer falling in the specified range', 'options:object?'), - ...variants$(StringOnly, 'isIP', args => 'matching to an IP', 'version:integer?'), - ...variants$(StringOnly, 'isIPRange', args => 'matching to an IP Range'), - ...variants$(StringOnly, 'isISBN', args => 'matching to an ISBN', 'version:integer?'), - ...variants$(StringOnly, 'isISIN', args => 'matching to an ISIN'), - ...variants$(StringOnly, 'isISO31661Alpha2', args => 'matching to a valid ISO 3166-1 alpha-2 officially assigned country code'), - ...variants$(StringOnly, 'isISO31661Alpha3', args => 'matching to a valid ISO 3166-1 alpha-3 officially assigned country code'), - ...variants$(StringOnly, 'isISO8601', args => 'matching to a valid ISO 8601 date'), - ...variants$(StringOnly, 'isISRC', args => 'matching to an ISRC'), - ...variants$(StringOnly, 'isISSN', args => 'matching to an ISSN', 'options:object?'), - ...variants$(StringOnly, 'isJSON', args => 'matching to a valid JSON'), - ...variants$(StringOnly, 'isJWT', args => 'matching to a valid JWT token'), - ...variants$(StringOrArray, 'isLatLong', 2, 'number', args => "representing a valid latitude-longitude coordinate in the format 'lat,long' or 'lat, long'"), - // ...variants$(StringOnly, 'isLength', args => 'whose length falls in the specified range'), - ...variants$(StringOnly, 'isLowercase', args => 'in lowercase'), - ...variants$(StringOnly, 'isMACAddress', args => 'in MAC address format'), - ...variants$(StringOnly, 'isMagnetURI', args => 'in magnet uri format'), - ...variants$(StringOnly, 'isMD5', args => 'representing a valid MD5 hash'), - ...variants$(StringOnly, 'isMimeType', args => 'matching to a valid MIME type format'), - ...variants$(StringOnly, 'isMobilePhone', args => 'representing a mobile phone number', 'locale:string|array?', 'options:object?'), - ...variants$(StringOnly, 'isMongoId', args => 'in the form of a valid hex-encoded representation of a MongoDB ObjectId.'), - ...variants$(StringOnly, 'isMultibyte', args => 'containing one or more multibyte chars'), - ...variants$(StringOnly, 'isNumeric', args => 'containing only numbers', 'options:object?'), - ...variants$(StringOrNumber, 'isPort', args => 'representing a valid port'), - ...variants$(StringOnly, 'isPostalCode', args => 'representing a postal code', 'options:object'), - ...variants$(StringOnly, 'isRFC3339', args => 'matching to a valid RFC 3339 date'), - ...variants$(StringOnly, 'isSurrogatePair', args => 'containing any surrogate pairs chars'), - ...variants$(StringOnly, 'isUppercase', args => 'in uppercase'), - ...variants$(StringOnly, 'isURL', args => 'representing a valid URL', 'options:object?'), - ...variants$(StringOnly, 'isUUID', args => 'matching to a UUID (version 3, 4 or 5)', 'version:integer?'), - ...variants$(StringOnly, 'isVariableWidth', args => 'containing a mixture of full and half-width chars'), - ...variants$(StringOnly, 'isWhitelisted', args => 'whose characters belongs to the whitelist', 'chars:string'), - ...variants$(StringOnly, 'matches', args => `matching the regex '${args[0]}'`, 'pattern:string|regex', 'modifiers:string?') + ...Info.variants(StringOnly, 'contains', args => `containing the value '${args[0]}'`, 'seed:string'), + // ...Info.variants(StringOnly, 'equals', args => `equal to the value '${args[0]}'`), + // ...Info.variants(StringOnly, 'isAfter', args => `equal to the value '${args[0]}'`), + ...Info.variants(StringOnly, 'isAlpha', args => 'containing only letters (a-zA-Z)', 'locale:string?'), + ...Info.variants(StringOnly, 'isAlphanumeric', args => 'containing only letters and numbers', 'locale:string?'), + ...Info.variants(StringOnly, 'isAscii', args => 'containing ASCII chars only'), + ...Info.variants(StringOnly, 'isBase64', args => 'base64 encoded'), + // ...Info.variants(StringOnly, 'isBefore', args => `equal to the value '${args[0]}'`), + // ...Info.variants(StringOnly, 'isBoolean', args => `equal to the value '${args[0]}'`), + ...Info.variants(StringOnly, 'isByteLength', args => 'whose length (in UTF-8 bytes) falls in the specified range', 'options:object?'), + ...Info.variants(StringOnly, 'isCreditCard', args => 'representing a credit card'), + ...Info.variants(StringOnly, 'isCurrency', args => 'representing a valid currency amount', 'options:object?'), + ...Info.variants(StringOnly, 'isDataURI', args => 'in data uri format'), + // ...Info.variants(StringOnly, 'isDecimal', args => `equal to the value '${args[0]}'`), + ...Info.variants(StringOrNumber, 'isDivisibleBy', args => `that's divisible by ${args[0]}`, 'divisor:integer'), + ...Info.variants(StringOnly, 'isEmail', args => 'representing an email address', 'options:object?'), + ...Info.variants(StringOnly, 'isEmpty', args => 'having a length of zero', 'options:object?'), + ...Info.variants(StringOrNumber, 'isFloat', args => 'that\'s a float falling in the specified range', 'options:object?'), + ...Info.variants(StringOnly, 'isFQDN', args => 'representing a fully qualified domain name (e.g. domain.com)', 'options:object?'), + ...Info.variants(StringOnly, 'isFullWidth', args => 'containing any full-width chars'), + ...Info.variants(StringOnly, 'isHalfWidth', args => 'containing any half-width chars'), + ...Info.variants(StringOnly, 'isHash', args => `matching to the format of the hash algorithm ${args[0]}`, 'algorithm:string?'), + ...Info.variants(StringOnly, 'isHexadecimal', args => 'representing a hexadecimal number'), + ...Info.variants(StringOnly, 'isHexColor', args => 'matching to a hexadecimal color'), + ...Info.variants(StringOnly, 'isIdentityCard', args => 'matching to a valid identity card code', 'locale:string?'), + // ...Info.variants(StringOnly, 'isIn', args => `equal to the value '${args[0]}'`), + ...Info.variants(StringOrNumber, 'isInt', args => 'that\'s an integer falling in the specified range', 'options:object?'), + ...Info.variants(StringOnly, 'isIP', args => 'matching to an IP', 'version:integer?'), + ...Info.variants(StringOnly, 'isIPRange', args => 'matching to an IP Range'), + ...Info.variants(StringOnly, 'isISBN', args => 'matching to an ISBN', 'version:integer?'), + ...Info.variants(StringOnly, 'isISIN', args => 'matching to an ISIN'), + ...Info.variants(StringOnly, 'isISO31661Alpha2', args => 'matching to a valid ISO 3166-1 alpha-2 officially assigned country code'), + ...Info.variants(StringOnly, 'isISO31661Alpha3', args => 'matching to a valid ISO 3166-1 alpha-3 officially assigned country code'), + ...Info.variants(StringOnly, 'isISO8601', args => 'matching to a valid ISO 8601 date'), + ...Info.variants(StringOnly, 'isISRC', args => 'matching to an ISRC'), + ...Info.variants(StringOnly, 'isISSN', args => 'matching to an ISSN', 'options:object?'), + ...Info.variants(StringOnly, 'isJSON', args => 'matching to a valid JSON'), + ...Info.variants(StringOnly, 'isJWT', args => 'matching to a valid JWT token'), + ...Info.variants(StringOrArray, 'isLatLong', 2, 'number', args => "representing a valid latitude-longitude coordinate in the format 'lat,long' or 'lat, long'"), + // ...Info.variants(StringOnly, 'isLength', args => 'whose length falls in the specified range'), + ...Info.variants(StringOnly, 'isLowercase', args => 'in lowercase'), + ...Info.variants(StringOnly, 'isMACAddress', args => 'in MAC address format'), + ...Info.variants(StringOnly, 'isMagnetURI', args => 'in magnet uri format'), + ...Info.variants(StringOnly, 'isMD5', args => 'representing a valid MD5 hash'), + ...Info.variants(StringOnly, 'isMimeType', args => 'matching to a valid MIME type format'), + ...Info.variants(StringOnly, 'isMobilePhone', args => 'representing a mobile phone number', 'locale:string|array?', 'options:object?'), + ...Info.variants(StringOnly, 'isMongoId', args => 'in the form of a valid hex-encoded representation of a MongoDB ObjectId.'), + ...Info.variants(StringOnly, 'isMultibyte', args => 'containing one or more multibyte chars'), + ...Info.variants(StringOnly, 'isNumeric', args => 'containing only numbers', 'options:object?'), + ...Info.variants(StringOrNumber, 'isPort', args => 'representing a valid port'), + ...Info.variants(StringOnly, 'isPostalCode', args => 'representing a postal code', 'options:object'), + ...Info.variants(StringOnly, 'isRFC3339', args => 'matching to a valid RFC 3339 date'), + ...Info.variants(StringOnly, 'isSurrogatePair', args => 'containing any surrogate pairs chars'), + ...Info.variants(StringOnly, 'isUppercase', args => 'in uppercase'), + ...Info.variants(StringOnly, 'isURL', args => 'representing a valid URL', 'options:object?'), + ...Info.variants(StringOnly, 'isUUID', args => 'matching to a UUID (version 3, 4 or 5)', 'version:integer?'), + ...Info.variants(StringOnly, 'isVariableWidth', args => 'containing a mixture of full and half-width chars'), + ...Info.variants(StringOnly, 'isWhitelisted', args => 'whose characters belongs to the whitelist', 'chars:string'), + ...Info.variants(StringOnly, 'matches', args => `matching the regex '${args[0]}'`, 'pattern:string|regex', 'modifiers:string?') ]; /* eslint-enable no-unused-vars */ diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 56d2209..d1ef355 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -1,7 +1,7 @@ const deepEqual = require('fast-deep-equal'); const lengthOf = require('@davebaol/length-of'); const bridge = require('./bridge'); -const { infoVariants$ } = require('../util/variants'); +const Info = require('../util/info'); const { getType } = require('../util/types'); // @@ -11,133 +11,175 @@ const { getType } = require('../util/types'); // - suffix $: first argument is a path in the object to validate // -function equals(info, arg, other, deep) { - const [aArg, oArg, dArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const oExpr = oArg.compile(other); - const dExpr = dArg.compile(deep); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!oExpr.resolved) { - if (oArg.resolve(oExpr, scope).error) { return info.error(oExpr.error); } - } - if (!dExpr.resolved) { - if (dArg.resolve(dExpr, scope).error) { return info.error(dExpr.error); } - } - const value = info.getValue(aExpr, scope); - const result = dExpr.result ? deepEqual(value, oExpr.result) : value === oExpr.result; - return result ? undefined : info.error(`expected a value equal to ${oExpr.result}`); - }; +/* eslint-disable lines-between-class-members */ + +class Equals extends Info { + constructor() { + super('equals', 'value:any', 'other:any', 'deep:boolean?'); + } + create() { + return (arg, other, deep) => { + const [aArg, oArg, dArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const oExpr = oArg.compile(other); + const dExpr = dArg.compile(deep); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!oExpr.resolved) { + if (oArg.resolve(oExpr, scope).error) { return this.error(oExpr.error); } + } + if (!dExpr.resolved) { + if (dArg.resolve(dExpr, scope).error) { return this.error(dExpr.error); } + } + const value = this.getValue(aExpr, scope); + const result = dExpr.result ? deepEqual(value, oExpr.result) : value === oExpr.result; + return result ? undefined : this.error(`expected a value equal to ${oExpr.result}`); + }; + }; + } } -function isLength(info, arg, options) { - const [aArg, optsArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const optsExpr = optsArg.compile(options); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!optsExpr.resolved) { - if (optsArg.resolve(optsExpr, scope).error) { return info.error(optsExpr.error); } - } - const opts = optsExpr.result; - const min = opts.min || 0; - const max = opts.max; // eslint-disable-line prefer-destructuring - const len = lengthOf(info.getValue(aExpr, scope)); - if (len === undefined) { - return info.error('expected a string, an array or an object'); - } - return len >= min && (max === undefined || len <= max) ? undefined : info.error(`expected string, array or object of length between ${opts.min} and ${opts.max}`); - }; +class IsLength extends Info { + constructor() { + super('isLength', 'value:any', 'options:object?'); + } + create() { + return (arg, options) => { + const [aArg, optsArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const optsExpr = optsArg.compile(options); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!optsExpr.resolved) { + if (optsArg.resolve(optsExpr, scope).error) { return this.error(optsExpr.error); } + } + const opts = optsExpr.result; + const min = opts.min || 0; + const max = opts.max; // eslint-disable-line prefer-destructuring + const len = lengthOf(this.getValue(aExpr, scope)); + if (len === undefined) { + return this.error('expected a string, an array or an object'); + } + return len >= min && (max === undefined || len <= max) ? undefined : this.error(`expected string, array or object of length between ${opts.min} and ${opts.max}`); + }; + }; + } } -function isSet(info, arg) { - const [aArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - return info.getValue(aExpr, scope) != null ? undefined : info.error(`the value at path '${arg}' must be set`); - }; +class IsSet extends Info { + constructor() { + super('isSet', 'value:any'); + } + create() { + return (arg) => { + const [aArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + return this.getValue(aExpr, scope) != null ? undefined : this.error(`the value at path '${arg}' must be set`); + }; + }; + } } -function isType(info, arg, type) { - const [aArg, tArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const tExpr = tArg.compile(type); - if (tExpr.resolved) { - tExpr.result = getType(tExpr.result); +class IsType extends Info { + constructor() { + super('isType', 'value:any', 'type:string|array'); } - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!tExpr.resolved) { - if (tArg.resolve(tExpr, scope).error) { return info.error(tExpr.error); } - try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { - return info.error(e.message); + create() { + return (arg, type) => { + const [aArg, tArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const tExpr = tArg.compile(type); + if (tExpr.resolved) { + tExpr.result = getType(tExpr.result); } - } - const t = tExpr.result; - return t.check(info.getValue(aExpr, scope)) ? undefined : info.error(`the value at path '${arg}' must be a '${t.name}'`); - }; + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!tExpr.resolved) { + if (tArg.resolve(tExpr, scope).error) { return this.error(tExpr.error); } + try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { + return this.error(e.message); + } + } + const t = tExpr.result; + return t.check(this.getValue(aExpr, scope)) ? undefined : this.error(`the value at path '${arg}' must be a '${t.name}'`); + }; + }; + } } -function isOneOf(info, arg, values) { - const [aArg, vArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const vExpr = vArg.compile(values); - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!vExpr.resolved) { - if (vArg.resolve(vExpr, scope).error) { return info.error(vExpr.error); } - } - return vExpr.result.includes(info.getValue(aExpr, scope)) ? undefined : info.error(`the value at path '${arg}' must be one of ${aExpr.result}`); - }; +class IsOneOf extends Info { + constructor() { + super('isOneOf', 'value:any', 'values:array'); + } + create() { + return (arg, values) => { + const [aArg, vArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const vExpr = vArg.compile(values); + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!vExpr.resolved) { + if (vArg.resolve(vExpr, scope).error) { return this.error(vExpr.error); } + } + return vExpr.result.includes(this.getValue(aExpr, scope)) ? undefined : this.error(`the value at path '${arg}' must be one of ${aExpr.result}`); + }; + }; + } } -function isArrayOf(info, arg, type) { - const [aArg, tArg] = info.argDescriptors; - const aExpr = aArg.compile(arg); - const tExpr = tArg.compile(type); - if (tExpr.resolved) { - tExpr.result = getType(tExpr.result); +class IsArrayOf extends Info { + constructor() { + super('isArrayOf', 'value:any', 'type:string|array'); } - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - if (!tExpr.resolved) { - if (tArg.resolve(tExpr, scope).error) { return info.error(tExpr.error); } - try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { - return info.error(e.message); + create() { + return (arg, type) => { + const [aArg, tArg] = this.argDescriptors; + const aExpr = aArg.compile(arg); + const tExpr = tArg.compile(type); + if (tExpr.resolved) { + tExpr.result = getType(tExpr.result); } - } - const value = info.getValue(aExpr, scope); - const t = tExpr.result; - if (!Array.isArray(value)) return info.error(`the value at path '${arg}' must be an array`); - const flag = value.every(e => t.check(e)); - return flag ? undefined : info.error(`the value at path '${arg}' must be an array of '${t.name}'`); - }; + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + if (!tExpr.resolved) { + if (tArg.resolve(tExpr, scope).error) { return this.error(tExpr.error); } + try { tExpr.result = scope.context.getType(tExpr.result); } catch (e) { + return this.error(e.message); + } + } + const value = this.getValue(aExpr, scope); + const t = tExpr.result; + if (!Array.isArray(value)) return this.error(`the value at path '${arg}' must be an array`); + const flag = value.every(e => t.check(e)); + return flag ? undefined : this.error(`the value at path '${arg}' must be an array of '${t.name}'`); + }; + }; + } } function leafValidators() { - /* eslint-disable no-unused-vars */ const vInfo = [ - ...infoVariants$(equals, 'value:any', 'other:any', 'deep:boolean?'), - ...infoVariants$(isArrayOf, 'value:any', 'type:string|array'), - ...infoVariants$(isLength, 'value:any', 'options:object?'), - ...infoVariants$(isOneOf, 'value:any', 'values:array'), - ...infoVariants$(isSet, 'value:any'), - ...infoVariants$(isType, 'value:any', 'type:string|array') + ...Info.variants(Equals), + ...Info.variants(IsArrayOf), + ...Info.variants(IsLength), + ...Info.variants(IsOneOf), + ...Info.variants(IsSet), + ...Info.variants(IsType) ]; - /* eslint-enable no-unused-vars */ const target = vInfo.reduce((acc, info) => { const k = info.name; diff --git a/src/util/info.js b/src/util/info.js index ac72090..17e85c2 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -1,33 +1,21 @@ +const camelCase = require('camelcase'); const Argument = require('./argument'); const { get } = require('../util/path'); +const { setFunctionName } = require('./misc'); class Info { - constructor(validator, ...argDescriptors) { - if (typeof validator === 'function') { - if (!validator.name) { - throw new Error('Expected non anonymous function; otherwise make sure it\'s not an issue due to minification'); - } - this.validator = validator; - this.name = validator.name.startsWith('_') ? validator.name.substring(1) : validator.name; - } else if (typeof validator === 'string') { - this.name = validator; - } else { - throw new Error('Expected the function or its name as first argument'); - } - this.is$ = this.name.endsWith('$'); - if (this.is$) { - this.baseName = this.name.substring(0, this.name.length - 1); - this.getValue = (expr, scope) => get(scope.find('$'), expr.result); - // will be processed in consolidate() - this.argDescriptors = ['path:path', ...argDescriptors.slice(1)]; - [this.originalArg0Desc] = argDescriptors; - } else { - this.baseName = this.name; - this.getValue = expr => expr.result; - // will be processed in consolidate() - this.argDescriptors = argDescriptors; - this.originalArg0Desc = undefined; - } + constructor(baseName, ...argDescriptors) { + this.baseName = baseName; + this.argDescriptors = argDescriptors; + } + + static variants(InfoClass, ...args) { + return [ + new InfoClass(...args).prepare(false, false), + new InfoClass(...args).prepare(false, true), + new InfoClass(...args).prepare(true, false), + new InfoClass(...args).prepare(true, true) + ]; } error(msg) { @@ -67,9 +55,9 @@ class Info { } /* eslint-disable-next-line class-methods-use-this */ - link() { + create() { /* istanbul ignore next */ - throw new Error('To support anonymous function inherited classes have to implement the method link!'); + throw new Error('Subclasses must implement the method create!'); } processArgDescriptors(context) { @@ -95,15 +83,45 @@ class Info { } } + variant(isOpt, is$, context) { + this.isOpt$ = !!isOpt; + this.is$ = !!is$; + this.name = isOpt ? camelCase(`opt ${this.baseName}`) : this.baseName; + if (isOpt) { + this.argDescriptors = [`${this.argDescriptors[0]}?`, ...this.argDescriptors.slice(1)]; + const original = this.create(); + this.create = () => (arg, ...args) => { + const aArg = this.argDescriptors[0]; + const aExpr = aArg.compile(arg); + this.compileRestParams(args, 1); // Make sure other arguments compile correctly + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } + } + return (this.getValue(aExpr, scope) ? original(aExpr.result, ...args)(scope) : undefined); + }; + }; + } + if (is$) { + this.name = `${this.name}$`; + this.getValue = (expr, scope) => get(scope.find('$'), expr.result); + [this.originalArg0Desc] = this.argDescriptors; + this.argDescriptors = ['path:path', ...this.argDescriptors.slice(1)]; + } else { + this.getValue = expr => expr.result; + this.originalArg0Desc = undefined; + } + this.processArgDescriptors(context); + return this.create(); + } + /* This method MUST be called before using the instance */ - consolidate(context) { - this.processArgDescriptors(context); - if (!this.validator) { - this.validator = this.link(); - } + prepare(isOpt, is$, context) { + this.validator = this.variant(isOpt, is$, context); this.validator.info = this; + setFunctionName(this.validator, this.name); return Object.freeze(this); // Return this for chaining } } diff --git a/src/util/variants.js b/src/util/variants.js deleted file mode 100644 index cba4771..0000000 --- a/src/util/variants.js +++ /dev/null @@ -1,88 +0,0 @@ -const camelCase = require('camelcase'); -const Info = require('./info'); -const { setFunctionName } = require('./misc'); - -function optShortcut(validator) { - const optValidator = (arg, ...args) => { - const { info } = optValidator; - const aArg = info.argDescriptors[0]; - const aExpr = aArg.compile(arg); - info.compileRestParams(args, 1); // Make sure other arguments compile correctly - return (scope) => { - if (!aExpr.resolved) { - if (aArg.resolve(aExpr, scope).error) { return info.error(aExpr.error); } - } - return (info.getValue(aExpr, scope) ? validator(aExpr.result, ...args)(scope) : undefined); - }; - }; - return setFunctionName(optValidator, camelCase(`opt ${validator.info.name}`)); -} - -function variant(InfoClass, validator, ...args) { - return new InfoClass(validator, ...args).consolidate(); -} - -function infoVariant(validator, ...args) { - return variant(Info, validator, ...args); -} - -function variantOpt(validator) { - const { argDescriptors } = validator.info; - return infoVariant( - optShortcut(validator), - ...[`${argDescriptors[0].name}:${argDescriptors[0].type.name}?`, ...argDescriptors.slice(1)] - ); -} - -function variants(InfoClass, validator, ...args) { - const mainVariant = variant(InfoClass, validator, ...args); - return [ - mainVariant, - variantOpt(mainVariant.validator) - ]; -} - -function infoVariants(validator, ...args) { - return variants(Info, validator, ...args); -} - -function getVariant(name, commonImpl) { - const f = (...args) => commonImpl(f.info, ...args); - return setFunctionName(f, name); -} - -function variants$(InfoClass, commonImpl, ...args) { - let validator; - let validator$; - if (typeof commonImpl === 'function') { - if (!commonImpl.name) { - throw new Error('Expected non anonymous function; otherwise make sure it\'s not an issue due to minification'); - } - validator = getVariant(commonImpl.name, commonImpl); - validator$ = getVariant(`${commonImpl.name}$`, commonImpl); - } else if (typeof commonImpl === 'string') { - validator = commonImpl; - validator$ = `${commonImpl}$`; - } else { - throw new Error('Expected either a named function or its name as first argument'); - } - return [ - ...variants(InfoClass, validator, ...args), - ...variants(InfoClass, validator$, ...args) - ]; -} - -function infoVariants$(commonImpl, ...args) { - return variants$(Info, commonImpl, ...args); -} - -module.exports = { - variant, - variants, - variants$, - infoVariant, - infoVariants, - infoVariants$, - variantOpt, - optShortcut -}; diff --git a/test/util/info.js b/test/util/info.js index c62056e..910b984 100644 --- a/test/util/info.js +++ b/test/util/info.js @@ -1,15 +1,30 @@ import { assert } from 'chai'; import Info from '../../src/util/info'; import Argument from '../../src/util/argument'; +import Scope from '../../src/util/scope'; import { V } from '../../src'; -describe('Test Info instance creation.', () => { - function validator(name) { - const v = () => undefined; - if (name) { - Object.defineProperty(v, 'name', { value: name, writable: false }); +describe('Test class Info.', () => { + it('Info should throw an error on prepare if any argument descriptors is neither a string, nor an object, nor an Argument instance', () => { + const info = new Info('badArg', []); + assert.throws(() => info.prepare(), Error, 'Invalid argument definition'); + }); + it('Info should throw an error on prepare if rest parameter is used before the last argument', () => { + const info = new Info('badRestParam', '...num:integer', 'tag:string'); + assert.throws(() => info.prepare(), Error, 'rest parameter'); + }); + it('Infoshould throw an error on prepare because method create is not implemented', () => { + const info = new Info('createNotImplemented', 'num:integer'); + assert.throws(() => info.prepare(), Error, 'create'); + }); +}); + +describe('Test ConcreteInfo subclass.', () => { + class ConcreteInfo extends Info { + /* eslint-disable-next-line class-methods-use-this */ + create() { + return (arg1, arg2) => scope => undefined; // eslint-disable-line no-unused-vars } - return v; } const stringArgs = ['myPath:path', 'num:integer?', '...rest:object']; const args = [ @@ -17,47 +32,74 @@ describe('Test Info instance creation.', () => { new Argument({ name: 'num', type: 'integer?' }), new Argument({ name: 'rest', type: 'object', restParams: true }) ]; - it('Info constructor should throw an error if 1st argument is an anonymous function', () => { - assert.throws(() => new Info(() => undefined, ...args), Error); + it('ConcreteInfo should be frozen once prepared', () => { + const info = new ConcreteInfo('frozen', ...args).prepare(); + assert(Object.isFrozen(info), ':('); }); - it('Info constructor should throw an error if 1st argument is neither a named function or its name', () => { - assert.throws(() => new Info({}, ...args), Error); + it('ConcreteInfo and its validator info should point each other', () => { + const info = new ConcreteInfo('circularReference', ...args).prepare(); + assert(info === info.validator.info, ':('); }); - it('Info should throw an error on consolidate if any argument descriptors is neither a string, nor an object, nor an Argument instance', () => { - const info = new Info(validator('namedValidator'), []); - assert.throws(() => info.consolidate(), Error, 'Invalid argument definition'); + it('Validator\'s name from info should match the function name', () => { + const info = new ConcreteInfo('myValidator', ...args).prepare(); + assert(info.validator.name === info.name, ':('); }); - it('Info should throw an error on consolidate if rest parameter is used before the last argument', () => { - const info = new Info(validator('badRestParam'), '...num:integer', 'tag:string'); - assert.throws(() => info.consolidate(), Error, 'rest parameter'); + it('Argument descriptors created from string should match the ones from corresponding Argument instances', () => { + const info1 = new ConcreteInfo('namedValidator', ...args).prepare(); + const info2 = new ConcreteInfo('namedValidator', ...stringArgs).prepare(); + assert.deepEqual(info1.argDescriptors, info2.argDescriptors, ':('); }); - it('Info created by name should throw an error on consolidate (method link not implemented)', () => { - const info = new Info('funcName', ...args); - assert.throws(() => info.consolidate(), Error, 'link'); + it('Invalid argument definition should throw an error on prepare', () => { + assert.throws(() => new ConcreteInfo('invalidArgDef', 123).prepare(), Error, 'Invalid argument definition'); }); - it('Info should be frozen once consolidated', () => { - const info = new Info(validator('namedValidator'), ...args); - info.consolidate(); - assert(Object.isFrozen(info), ':('); +}); + +describe('Test opt and $ variants.', () => { + class TestInfo extends Info { + /* eslint-disable-next-line class-methods-use-this */ + create() { + return arg1 => scope => 'done'; // eslint-disable-line no-unused-vars + } + } + it('Opt variant of xyz should be optXyz', () => { + const info = new TestInfo('xyz', 'num:integer').prepare(true); + assert(info.name === 'optXyz', ':('); }); - it('Validator and its info should point each other', () => { - const v = validator('namedValidator'); - const info = new Info(v, ...args); - info.consolidate(); - assert(v.info === info && v === info.validator, ':('); + it('optXyz(a): should allow null as 1st argument', () => { + const info = new TestInfo('xyz', 'num:integer').prepare(true); + assert(info.argDescriptors[0].type.check(null), ':('); }); - it('Validator\'s name from info should match the function name', () => { - const v = validator('namedValidator'); - const info = new Info(v, ...args); - info.consolidate(); - assert(v.name === info.name, ':('); + it('optXyz(a1): bad value as 1st argument should throw an error at compile time', () => { + const info = new TestInfo('xyz', 'num:integer').prepare(true); + assert.throws(() => info.validator(() => null), Error, 'Expected type \'null|integer\''); }); - it('Argument descriptors created from string should match the ones from corresponding Argument instances', () => { - const info1 = new Info(validator('namedValidator'), ...args); - info1.consolidate(); - const info2 = new Info(validator('namedValidator'), ...stringArgs); - info2.consolidate(); - assert.deepEqual(info1.argDescriptors, info2.argDescriptors, ':('); + it('optXyz(a1, a2): Bad value as 2nd argument should throw an error at compile time', () => { + const info = new TestInfo('xyz', 'num:integer', 'values:string|array').prepare(true); + assert.throws(() => info.validator(3, 2), Error, 'Expected type \'string|array\''); + }); + it('optXyz$(path): missing property at path should succeed', () => { + const info = new TestInfo('xyz', 'value:integer').prepare(true, true); + const v = info.validator('a'); + assert(v(new Scope({})) === undefined, ':('); + }); + it('optXyz$(path): not missing property at path should always match xyz$(path)', () => { + const info1 = new TestInfo('xyz', 'value:integer').prepare(false, true); + const info2 = new TestInfo('xyz', 'value:integer').prepare(true, true); + const v1 = info1.validator('a'); + const v2 = info2.validator('a'); + assert(v1(new Scope({ a: 123 })) === v2(new Scope({ a: 123 })), ':('); + }); + it('optXyz$(path): missing property at referenced path should always succeed', () => { + const info = new TestInfo('xyz', 'value:integer').prepare(true, true); + const v = V.def({ p: 'a' }, info.validator({ $var: 'p' })); + assert(v(new Scope({})) === undefined, ':('); + }); + it('optXyz$(path): not missing property at referenced path should always match xyz$(path)', () => { + const info1 = new TestInfo('xyz', 'value:integer').prepare(false, true); + const info2 = new TestInfo('xyz', 'value:integer').prepare(true, true); + const v1 = V.def({ p: 'a' }, info1.validator({ $var: 'p' })); + const v2 = V.def({ p: 'a' }, info2.validator({ $var: 'p' })); + assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); }); }); diff --git a/test/util/scope.js b/test/util/scope.js index 7421a30..e39187a 100644 --- a/test/util/scope.js +++ b/test/util/scope.js @@ -7,6 +7,10 @@ describe('Test utility Scope.compile($, scope).', () => { const defs = { $var: 'MY_OTHER_SCOPE' }; assert.throws(() => Scope.compile(null, defs), Error, 'Root reference not allowed'); }); + it('Should throw an error if $ is shadowed ', () => { + const defs = { $: 'invalid variable' }; + assert.throws(() => Scope.compile(null, defs), Error, '$ cannot be shadowed'); + }); it('Should return the same scope specified in input, if all its variables have no references (constants)', () => { const defs = { VARIABLE: 123 }; assert(Scope.compile(null, defs).resources === defs, ':('); diff --git a/test/util/variants.js b/test/util/variants.js deleted file mode 100644 index e264c1b..0000000 --- a/test/util/variants.js +++ /dev/null @@ -1,61 +0,0 @@ -import { assert } from 'chai'; -import { V, Scope } from '../../src'; -import { infoVariants, infoVariants$, variantOpt } from '../../src/util/variants'; - -describe('Test optShortcut().', () => { - function optOf(validator) { - return variantOpt(validator).validator; - } - - it('Function xyz should become optXyz', () => { - const optIsSet = optOf(V.isSet); - assert(optIsSet.info.name === 'optIsSet', ':('); - }); - it('Bad value as 1st argument should throw an error at compile time', () => { - const optIsType = optOf(V.isType); - assert.throws(() => optIsType(() => null, 'string'), Error, 'Expected type \'any\''); - }); - it('Bad value as 2nd argument should throw an error at compile time', () => { - const optIsType = optOf(V.isType); - assert.throws(() => optIsType('a', 2), Error, 'Expected type \'string|array\''); - }); - it('Missing property at path should succeed', () => { - const optIsSet = optOf(V.isSet); - const v = optIsSet('a'); - assert(v(new Scope({})) === undefined, ':('); - }); - it('Not missing property at path should always match the original validator', () => { - const optIsSet = optOf(V.isSet); - const v1 = optIsSet('a'); - const v2 = V.isSet('a'); - assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); - }); - it('Missing property at referenced path should always succeed', () => { - const optIsSet = optOf(V.isSet); - const v = V.def({ p: 'a' }, optIsSet({ $var: 'p' })); - assert(v(new Scope({})) === undefined, ':('); - }); - it('Not missing property at referenced path should always match the original validator', () => { - const optIsSet = optOf(V.isSet); - const v1 = V.def({ p: 'a' }, optIsSet({ $var: 'p' })); - const v2 = V.def({ p: 'a' }, V.optIsSet({ $var: 'p' })); - assert(v1(new Scope({ a: 0 })) === v2(new Scope({ a: 32 })), ':('); - }); -}); - -describe('Test infoVariants().', () => { - it('Passing a named function should return the info for that function and its opt shortcut', () => { - function foo(any) { return any; } - const info = infoVariants(foo, 'value:any'); - assert.deepEqual(info.map(i => i.validator.name), ['foo', 'optFoo'], ':('); - }); -}); - -describe('Test infoVariants$().', () => { - it('Passing something other than a named function or its name should throw an error', () => { - assert.throws(() => infoVariants$(123), Error, 'Expected either a named function or its name as first argument'); - }); - it('Passing a non named function should throw an error', () => { - assert.throws(() => infoVariants$(() => null), Error, 'Expected non anonymous function'); - }); -}); From b332b3d0457c2f66da9b358192be441498cabc89 Mon Sep 17 00:00:00 2001 From: davebaol Date: Mon, 24 Jun 2019 18:42:19 +0200 Subject: [PATCH 16/16] Fix dsl-validator.yaml --- examples/dsl-validator.yaml | 52 ++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/examples/dsl-validator.yaml b/examples/dsl-validator.yaml index fd7e85f..226ee78 100644 --- a/examples/dsl-validator.yaml +++ b/examples/dsl-validator.yaml @@ -15,31 +15,34 @@ def: - isOneOf$: [key, {$var: validatorNames}] # validator names are defined in the root scope and passed in by the code launching the validation - isType$: [value, array] - isLength$: [value, {min: 1}] - - call$: [value.0, {$var: $ARG_PATH}] + - if: + - matches: [key, '\$$'] + - call$: [value.0, {$var: $ARG_PATH}] + - call$: [value.0, {$var: $ARG_ANY}] $BRANCH_VALIDATOR: xor: - - call$: [alter, {$var: $ARG_ONE_CHILD_AND_TWO_RESULTS}] + - call$: [alter, {$var: $ARG_TWO_ANY_AND_ONE_CHILD}] - call$: [and, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] - - call$: [call, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [call, {$var: $ARG_ANY_AND_ONE_CHILD}] - call$: [call$, {$var: $ARG_PATH_AND_ONE_CHILD}] - call$: [def, {$var: $ARG_SCOPE_AND_ONE_CHILD}] - - call$: [every, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [every, {$var: $ARG_ANY_AND_ONE_CHILD}] - call$: [every$, {$var: $ARG_PATH_AND_ONE_CHILD}] - call$: [if, {$var: $ARG_TWO_OR_THREE_CHILDREN}] - call$: [not, {$var: $ARG_ONE_CHILD}] - - call$: [optCall, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optCall, {$var: $ARG_ANY_AND_ONE_CHILD}] - call$: [optCall$, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call$: [optEvery, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optEvery, {$var: $ARG_ANY_AND_ONE_CHILD}] - call$: [optEvery$, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call$: [optSome, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [optSome, {$var: $ARG_ANY_AND_ONE_CHILD}] - call$: [optSome$, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call$: [optWhile, {$var: $ARG_PATH_AND_TWO_CHILDREN}] + - call$: [optWhile, {$var: $ARG_ANY_AND_TWO_CHILDREN}] - call$: [optWhile$, {$var: $ARG_PATH_AND_TWO_CHILDREN}] - call$: [or, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] - - call$: [some, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [some, {$var: $ARG_ANY_AND_ONE_CHILD}] - call$: [some$, {$var: $ARG_PATH_AND_ONE_CHILD}] - - call$: [while, {$var: $ARG_PATH_AND_TWO_CHILDREN}] + - call$: [while, {$var: $ARG_ANY_AND_TWO_CHILDREN}] - call$: [while$, {$var: $ARG_PATH_AND_TWO_CHILDREN}] - call$: [xor, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] @@ -47,7 +50,6 @@ def: or: - isType$: ['', [string, number, 'null']] - isArrayOf$: ['', [string, number]] - - optIsSet$: [''] # Always true!!!! Temporary hack to be removed. $ARG_REF: and: @@ -73,17 +75,22 @@ def: - matches$: [key, ^\$.] - call$: [value, {$var: $VALIDATOR}] + $ARG_ANY: + or: + - $var: $ARG_REF + - isType$: ['', any] + $ARG_STRING: or: - $var: $ARG_REF - isType$: ['', string] - $ARG_ONE_CHILD_AND_TWO_RESULTS: + $ARG_TWO_ANY_AND_ONE_CHILD: and: - isType$: ['', array] - - call$: [0, {$var: $VALIDATOR}] - - optCall$: [1, {$var: $ARG_STRING}] - - optCall$: [2, {$var: $ARG_STRING}] + - call$: [0, {$var: $ARG_ANY}] + - call$: [1, {$var: $ARG_ANY}] + - call$: [2, {$var: $VALIDATOR}] $ARG_SCOPE_AND_ONE_CHILD: and: @@ -92,6 +99,13 @@ def: - optCall$: [0, {$var: $ARG_SCOPE}] - call$: [1, {$var: $VALIDATOR}] + $ARG_ANY_AND_ONE_CHILD: + and: + - isType$: ['', array] + - isLength$: ['', {min: 2, max: 2}] + - call$: [0, {$var: $ARG_ANY}] + - call$: [1, {$var: $VALIDATOR}] + $ARG_PATH_AND_ONE_CHILD: and: - isType$: ['', array] @@ -99,6 +113,14 @@ def: - call$: [0, {$var: $ARG_PATH}] - call$: [1, {$var: $VALIDATOR}] + $ARG_ANY_AND_TWO_CHILDREN: + and: + - isType$: ['', array] + - isLength$: ['', {min: 3, max: 3}] + - call$: [0, {$var: $ARG_ANY}] + - call$: [1, {$var: $VALIDATOR}] + - call$: [2, {$var: $VALIDATOR}] + $ARG_PATH_AND_TWO_CHILDREN: and: - isType$: ['', array]