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..54a25ce 100644 --- a/examples/data-driven.js +++ b/examples/data-driven.js @@ -2,7 +2,7 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); -const ensureValidator = require("../lib/ensure-validator"); +const { Scope, compile } = require("../lib"); let toBeValidated = { a: { @@ -14,9 +14,9 @@ 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(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/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 b770139..011b65e 100644 --- a/examples/dsl-validator.js +++ b/examples/dsl-validator.js @@ -2,14 +2,14 @@ const path = require("path"); const fs = require("fs"); const yaml = require("js-yaml"); -const ensureValidator = require("../lib/ensure-validator"); +const { V, 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')); -let vError = validator(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..226ee78 100644 --- a/examples/dsl-validator.yaml +++ b/examples/dsl-validator.yaml @@ -1,52 +1,63 @@ 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}] + - 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: [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_TWO_ANY_AND_ONE_CHILD}] + - call$: [and, {$var: $ARG_ZERO_OR_MORE_CHILDREN}] + - 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_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_ANY_AND_ONE_CHILD}] + - call$: [optCall$, {$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_ANY_AND_ONE_CHILD}] + - call$: [optSome$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - 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_ANY_AND_ONE_CHILD}] + - call$: [some$, {$var: $ARG_PATH_AND_ONE_CHILD}] + - call$: [while, {$var: $ARG_ANY_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]] $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 +68,86 @@ 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_ANY: + or: + - $var: $ARG_REF + - isType$: ['', any] $ARG_STRING: or: - $var: $ARG_REF - - isType: ['', string] + - 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}] + - isType$: ['', array] + - call$: [0, {$var: $ARG_ANY}] + - call$: [1, {$var: $ARG_ANY}] + - call$: [2, {$var: $VALIDATOR}] $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_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] - - 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_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] - - 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 44d6785..8ee1ba6 100644 --- a/examples/hard-coded.js +++ b/examples/hard-coded.js @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -const path = require("path"); -const V = require('../lib'); +const path = require('path'); +const { V, Scope } = require('../lib'); let toBeValidated = { a: { @@ -12,18 +12,18 @@ 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 ); // 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/package.json b/package.json index e36ec2a..f6f2d87 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", @@ -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", @@ -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": { diff --git a/src/branch-validators.js b/src/branch-validators.js index 34cf5eb..1290269 100644 --- a/src/branch-validators.js +++ b/src/branch-validators.js @@ -1,389 +1,463 @@ -const { get } = require('./util/path'); -const Scope = require('./util/scope'); -const createShortcuts = require('./util/create-shortcuts'); const Info = require('./util/info'); +const Scope = require('./util/scope'); // // 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); - const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope, obj); - if (cExpr.error) { return cExpr.error; } - } - return cExpr.result(get(obj, pExpr.result), scope); - }; -} +/* eslint-disable lines-between-class-members */ -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 cExpr = infoArgs[1].compile(child); - return (obj, scope) => { - if (!childScope.parent) { - childScope.setParent(scope); - } - if (!childScope.resolved) { // Let's process references - try { - childScope.resolve(obj); - } catch (e) { - return e.message; - } - } - if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, childScope, obj); - if (cExpr.error) { return cExpr.error; } - } - return cExpr.result(obj, childScope); - }; +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 not(child) { - const infoArgs = not.info.argDescriptors; - const cExpr = infoArgs[0].compile(child); - return (obj, scope = new Scope()) => { - if (!cExpr.resolved) { - infoArgs[0].resolve(cExpr, scope, obj); - if (cExpr.error) { return cExpr.error; } - } - return cExpr.result(obj, scope) ? undefined : 'not: the child validator must fail'; - }; +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 and(...children) { - const { info } = and; - const childArg = info.argDescriptors[0]; - const offspring = info.compileRestParams(children); - return (obj, scope = new Scope()) => { - for (let i = 0, len = offspring.length; i < len; i += 1) { - const cExpr = offspring[i]; - if (!cExpr.resolved) { - childArg.resolve(cExpr, scope, obj); - if (cExpr.error) { return cExpr.error; } - } - const error = (cExpr.result)(obj, scope); // Validate child - if (error) { - return error; - } - } - return undefined; - }; +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 or(...children) { - const { info } = or; - const childArg = info.argDescriptors[0]; - const offspring = info.compileRestParams(children); - return (obj, scope = new 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); - if (cExpr.error) { return cExpr.error; } - } - error = (cExpr.result)(obj, scope); // Validate child - if (!error) { +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; - } - } - return error; - }; + }; + }; + } } -function xor(...children) { - const { info } = xor; - const childArg = info.argDescriptors[0]; - const offspring = info.compileRestParams(children); - return (obj, scope = new 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); - if (cExpr.error) { return cExpr.error; } - } - const error = (cExpr.result)(obj, scope); // Validate child - count += error ? 0 : 1; - if (count === 2) { - break; - } - } - return count === 1 ? undefined : `xor: expected exactly 1 valid child; found ${count} instead`; - }; +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; + }; + }; + } } -// 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); - return (obj, scope = new Scope()) => { - if (!ccExpr.resolved) { - infoArgs[0].resolve(ccExpr, scope, obj); - if (ccExpr.error) { return ccExpr.error; } - } - if (!tcExpr.resolved) { - infoArgs[1].resolve(tcExpr, scope, obj); - if (tcExpr.error) { return tcExpr.error; } - } - if (!ecExpr.resolved) { - infoArgs[2].resolve(ecExpr, scope, obj); - if (ecExpr.error) { return ecExpr.error; } - } - if (ecExpr.result == null) { - return ccExpr.result(obj, scope) ? undefined : tcExpr.result(obj, scope); - } - // either then or else is validated, not both! - return (ccExpr.result(obj, scope) ? ecExpr.result : tcExpr.result)(obj, scope); - }; +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`); + }; + }; + } } -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()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - 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); - if (Array.isArray(value)) { - let error; - const found = value.find((item, index) => { - error = cExpr.result({ index, value: item, original: obj }, scope); - return error; - }); - return found ? error : undefined; - } - if (typeof value === 'object') { - let error; - const found = Object.keys(value).find((key, index) => { - error = cExpr.result({ - index, key, value: value[key], original: obj - }, scope); - return error; - }); - return found ? error : undefined; - } - if (typeof value === 'string') { - 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; - } - } - return error; - } - return `every: the value at path '${path}' must be either a string, an array or an object; found type '${typeof value}'`; - }; +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); + }; + }; + } +} + +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(path, child) { - const infoArgs = some.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - 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); - if (Array.isArray(value)) { - let error; - const found = value.find((item, index) => { - error = cExpr.result({ index, value: item, original: obj }, scope); - return !error; - }); - return found ? undefined : error; - } - if (typeof value === 'object') { - let error; - const found = Object.keys(value).find((key, index) => { - error = cExpr.result({ - index, key, value: value[key], original: obj - }, scope); - return !error; - }); - return found ? undefined : error; - } - if (typeof value === 'string') { - 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; - } - } - return error; - } - return `some: the value at path '${path}' 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 infoArgs = alter.info.argDescriptors; - const sExpr = infoArgs[0].compile(resultOnSuccess); - const fExpr = infoArgs[1].compile(resultOnError); - const cExpr = infoArgs[2].compile(child); - return (obj, scope = new Scope()) => { - if (!sExpr.resolved) { - infoArgs[0].resolve(sExpr, scope, obj); - if (sExpr.error) { return sExpr.error; } - } - if (!fExpr.resolved) { - infoArgs[1].resolve(fExpr, scope, obj); - if (fExpr.error) { return fExpr.error; } - } - if (!cExpr.resolved) { - infoArgs[2].resolve(cExpr, scope, obj); - if (cExpr.error) { return cExpr.error; } - } - const r = cExpr.result(obj, 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 infoArgs = onError.info.argDescriptors; - const rExpr = infoArgs[0].compile(result); - const cExpr = infoArgs[1].compile(child); - return (obj, scope = new Scope()) => { - if (!rExpr.resolved) { - infoArgs[0].resolve(rExpr, scope, obj); - if (rExpr.error) { return rExpr.error; } - } - if (!cExpr.resolved) { - infoArgs[1].resolve(cExpr, scope, obj); - if (cExpr.error) { return cExpr.error; } - } - if (cExpr.result(obj, 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(path, condChild, doChild) { - const infoArgs = _while.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const ccExpr = infoArgs[1].compile(condChild); - const dcExpr = infoArgs[2].compile(doChild); - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!ccExpr.resolved) { - infoArgs[1].resolve(ccExpr, scope, obj); - if (ccExpr.error) { return ccExpr.error; } - } - if (!dcExpr.resolved) { - infoArgs[2].resolve(dcExpr, scope, obj); - if (dcExpr.error) { return dcExpr.error; } - } - const value = get(obj, pExpr.result); - const status = { succeeded: 0, failed: 0, original: obj }; - if (Array.isArray(value)) { - let error; - const found = value.find((item, index) => { - status.index = index; - status.value = item; - error = ccExpr.result(status, scope); - if (!error) { - status.failed += dcExpr.result(status, 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; - }); - return found ? error : undefined; - } - if (typeof value === 'object') { - 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); - if (!error) { - status.failed += dcExpr.result(status, 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; - }); - return found ? error : undefined; - } - if (typeof value === 'string') { - 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; - status.succeeded = index + 1 - status.failed; - } - return error; - } - return `while: the value at path '${path}' 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 */ - /* 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') + ...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) => { - 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/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..326aab1 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'); + +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/leaf-validators/bridge.js b/src/leaf-validators/bridge.js index e1f847b..f49dd22 100644 --- a/src/leaf-validators/bridge.js +++ b/src/leaf-validators/bridge.js @@ -1,14 +1,7 @@ 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) { - super(name, ...(['path:path'].concat(noPathArgDescriptors))); - 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. @@ -16,20 +9,18 @@ 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,9 +30,14 @@ const SPECIALIZED_VALIDATORS = { } }; -class StringOnly extends Bridge { +class Bridge extends Info { + constructor(name, errorFunc, firstArgDescriptor, ...otherArgDescriptors) { + super(name, ...[firstArgDescriptor, ...otherArgDescriptors]); + this.errorFunc = errorFunc; + } + // 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; } @@ -50,48 +46,60 @@ class StringOnly extends Bridge { 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() { - 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); + create() { + const original = v[this.baseName]; + const specialized = SPECIALIZED_VALIDATORS[this.baseName]; + return (arg, ...restArgs) => { + const aArg = this.argDescriptors[0]; + const aExpr = aArg.compile(arg); + const restExpr = this.compileRestParams(restArgs, 1); const restValue = []; - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - this.argDescriptors[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } + return (scope) => { + if (!aExpr.resolved) { + if (aArg.resolve(aExpr, scope).error) { return this.error(aExpr.error); } } - const errorAt = this.resolveRestParams(restExpr, 1, scope, obj); - if (errorAt >= 0) { return restExpr[errorAt].error; } + const errorAt = this.resolveRestParams(restExpr, 1, scope); + 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; } - let value = get(obj, 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.genericError(arg, restArgs); }; }; } } -class StringAndNumber extends StringOnly { +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 - isSpecialized(value) { + isSpecializedFor(value) { return typeof value === 'number'; } @@ -99,20 +107,16 @@ class StringAndNumber extends StringOnly { 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 StringAndArray 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; } - isSpecialized(value) { + isSpecializedFor(value) { return Array.isArray(value) && (this.length === undefined || value.length === this.length) // eslint-disable-next-line valid-typeof @@ -123,85 +127,80 @@ class StringAndArray extends StringOnly { 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 = [ - 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?') + ...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 */ 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 // 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 } }); diff --git a/src/leaf-validators/index.js b/src/leaf-validators/index.js index 9ffd99f..d1ef355 100644 --- a/src/leaf-validators/index.js +++ b/src/leaf-validators/index.js @@ -1,157 +1,187 @@ 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 Scope = require('../util/scope'); 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); - const dExpr = infoArgs[2].compile(deep); - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!vExpr.resolved) { - infoArgs[1].resolve(vExpr, scope, obj); - if (vExpr.error) { return vExpr.error; } - } - if (!dExpr.resolved) { - infoArgs[2].resolve(dExpr, scope, obj); - if (dExpr.error) { return dExpr.error; } - } - const result = dExpr.result - ? deepEqual(get(obj, pExpr.result), vExpr.result) - : get(obj, pExpr.result) === vExpr.result; - return result ? undefined : `equals: the value at path '${path}' must be equal to ${vExpr.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(path, options) { - const infoArgs = isLength.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const optsExpr = infoArgs[1].compile(options); - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!optsExpr.resolved) { - infoArgs[1].resolve(optsExpr, scope, obj); - 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)); - if (len === undefined) { - return `isLength: the value at path '${path}' must be 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}`; - }; +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(path) { - const infoArgs = isSet.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - return get(obj, pExpr.result) != null ? undefined : `isSet: the value at path '${path}' 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(path, type) { - const infoArgs = isType.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const tExpr = infoArgs[1].compile(type); - if (tExpr.resolved) { - tExpr.result = getType(tExpr.result); +class IsType extends Info { + constructor() { + super('isType', 'value:any', 'type:string|array'); + } + 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); + } + 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}'`); + }; + }; } - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!tExpr.resolved) { - infoArgs[1].resolve(tExpr, scope, obj); - 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}'`; - }; } -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()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!aExpr.resolved) { - infoArgs[1].resolve(aExpr, scope, obj); - 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}`; - }; +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(path, type) { - const infoArgs = isType.info.argDescriptors; - const pExpr = infoArgs[0].compile(path); - const tExpr = infoArgs[1].compile(type); - if (tExpr.resolved) { - tExpr.result = getType(tExpr.result); +class IsArrayOf extends Info { + constructor() { + super('isArrayOf', 'value:any', 'type:string|array'); + } + 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); + } + 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}'`); + }; + }; } - return (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - infoArgs[0].resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - if (!tExpr.resolved) { - infoArgs[1].resolve(tExpr, scope, obj); - 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 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)); - return flag ? undefined : `isArrayOf: the value at path '${path}' 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') + ...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) => { - info.consolidate(); const k = info.name; acc[k] = info.validator; // eslint-disable-line no-param-reassign return acc; @@ -160,9 +190,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/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 deleted file mode 100644 index 9e0321b..0000000 --- a/src/util/create-shortcuts.js +++ /dev/null @@ -1,51 +0,0 @@ -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; - 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 (obj, scope = new Scope()) => { - if (!pExpr.resolved) { - argDescriptor0.resolve(pExpr, scope, obj); - if (pExpr.error) { return pExpr.error; } - } - return (get(obj, pExpr.result) ? validator(pExpr.result, ...args)(obj, 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/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..17e85c2 100644 --- a/src/util/info.js +++ b/src/util/info.js @@ -1,19 +1,25 @@ +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.argDescriptors = argDescriptors; // will be processed in consolidate() + 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) { + return `${this.name}: ${msg}`; } compileRestParams(args, offset = 0) { @@ -27,12 +33,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); - if (exprs[i].error) { + if (ad.resolve(exprs[i], scope).error) { return i; } } @@ -50,37 +55,74 @@ 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) { const last = this.argDescriptors.length - 1; + this.isLeaf = true; this.argDescriptors = this.argDescriptors.map((d, i) => { let a; 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); + } + } + + 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; - Object.freeze(this); + setFunctionName(this.validator, this.name); + return Object.freeze(this); // Return this for chaining } } diff --git a/src/util/misc.js b/src/util/misc.js index fc992aa..29720f7 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; @@ -23,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) { @@ -33,5 +39,7 @@ function lazyProperty(instance, key, value, writable, configurable) { module.exports = { ANY_VALUE, checkUniqueKey, - lazyProperty + clone, + lazyProperty, + setFunctionName }; diff --git a/src/util/scope.js b/src/util/scope.js index ce136e5..ba896ee 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\''); } @@ -34,30 +43,33 @@ 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('$') ? child : any; - const ref = type.compile(cur); + 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; } } } } - 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 @@ -69,7 +81,7 @@ class Scope { let resource = compiledResources[k]; if (resource instanceof Expression) { const type = k.startsWith('$') ? child : any; - const ref = type.resolve(resource, this, obj); + const ref = type.resolve(resource, this); if (ref.error) { throw new Error(ref.error); } resource = ref.result; } @@ -77,7 +89,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..ac2e9c2 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 = {}; @@ -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`); @@ -229,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); @@ -253,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) { // clone 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/branch-validators/call.js b/test/branch-validators/call.js index 39932c0..b3cca42 100644 --- a/test/branch-validators/call.js +++ b/test/branch-validators/call.js @@ -1,12 +1,12 @@ -import V from '../../src'; +import { V } from '../../src'; 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 92cb754..e3dee6d 100644 --- a/test/branch-validators/def.js +++ b/test/branch-validators/def.js @@ -1,40 +1,40 @@ import { assert } from 'chai'; -import V from '../../src'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; 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('')); - assert(v({ a: 123 }) === undefined, ':('); + 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' }); - assert(v({ 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(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, ':('); + 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', () => { const v = V.def( { v1: 123 }, V.def( { v2: { $var: 'v1' } }, - { equals: ['a', { $var: 'v2' }] } + { 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( @@ -42,9 +42,9 @@ describe('Test branch validator def.', () => { v1: 123, v2: { $var: 'v1' } }, - { equals: ['a', { $var: 'v2' }] } + { 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( @@ -52,21 +52,21 @@ describe('Test branch validator def.', () => { v2: { $var: 'v1' }, v1: 123 }, - { equals: ['a', { $var: 'v2' }] } + { 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( { var1: 'a', - $validator1: { equals: [{ $var: 'var1' }, 123] } + $validator1: { equals$: [{ $var: 'var1' }, 123] } }, V.def( { var1: 'b' }, { $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..6df6b7d 100644 --- a/test/branch-validators/if.js +++ b/test/branch-validators/if.js @@ -1,11 +1,11 @@ import { assert } from 'chai'; -import V from '../../src'; +import { V, Scope } from '../../src'; 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] }; @@ -14,25 +14,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..2efef0f 100644 --- a/test/branch-validators/iterators.js +++ b/test/branch-validators/iterators.js @@ -1,6 +1,6 @@ import { assert } from 'chai'; import lengthOf from '@davebaol/length-of'; -import V from '../../src'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; @@ -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; @@ -29,7 +29,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 +37,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,40 +59,40 @@ 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, ':('); })); }); } -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 vDo = (obj) => { actual.push(Object.assign({}, obj)); return undefined; }; - const v = V.while(type, vCond, vDo); - v(test); + const vCond = V.optIsSet$(''); // always true + const vDo = (scope) => { actual.push(Object.assign({}, scope.find('$'))); return undefined; }; + const v = V.while$(type, vCond, vDo); + v(new Scope(test)); assert.deepEqual(actual, expected, ':('); }); } @@ -107,9 +107,9 @@ 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, ':('); + Object.keys(failureExpected).forEach(k => it(`For ${k} while$ should fail`, () => { + const v = V.while$('', () => undefined, () => undefined); + assert(v(new Scope(failureExpected[k])) !== undefined, ':('); })); function checkParents(shouldSucceed) { @@ -123,12 +123,13 @@ 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) ); - 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/logical-operators.js b/test/branch-validators/logical-operators.js index 1cec906..0bcfc3d 100644 --- a/test/branch-validators/logical-operators.js +++ b/test/branch-validators/logical-operators.js @@ -1,10 +1,10 @@ -import V from '../../src'; +import { V } from '../../src'; 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 20e0b04..df6716f 100644 --- a/test/branch-validators/misc.js +++ b/test/branch-validators/misc.js @@ -1,10 +1,10 @@ import { assert } from 'chai'; -import V from '../../src'; +import { V, Scope } from '../../src'; 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.', () => { @@ -13,11 +13,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 +26,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/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'); + }); +}); 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 8c453ca..6c0c364 100644 --- a/test/leaf-validators/equals.js +++ b/test/leaf-validators/equals.js @@ -1,30 +1,31 @@ import { assert } from 'chai'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V, Scope } from '../../src'; 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); - assert(b === null ? v(obj2) !== undefined : v(obj2) === undefined, ':('); + 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' })); - assert(v(obj2) === undefined, ':('); + 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 455c291..09d3167 100644 --- a/test/leaf-validators/isLength.js +++ b/test/leaf-validators/isLength.js @@ -1,5 +1,5 @@ import { assert } from 'chai'; -import V from '../../src'; +import { V, Scope } from '../../src'; import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; const { SUCCESS, FAILURE } = VALIDATION; @@ -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: { $path: '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 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 = 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 15b72d6..a320acc 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; @@ -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 e667b3f..abb0d63 100644 --- a/test/leaf-validators/isPort.js +++ b/test/leaf-validators/isPort.js @@ -1,14 +1,14 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V } from '../../src'; 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 a727ba1..bb9d8ca 100644 --- a/test/leaf-validators/isSet.js +++ b/test/leaf-validators/isSet.js @@ -1,14 +1,14 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +import { V } from '../../src'; 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 d2c4c6e..dd0f6fc 100644 --- a/test/leaf-validators/type-checkers.js +++ b/test/leaf-validators/type-checkers.js @@ -1,34 +1,34 @@ import { testAllArguments, testValidation, VALIDATION } from '../test-utils'; -import V from '../../src'; +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/test-utils.js b/test/test-utils.js index cb594d3..52fceae 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -1,6 +1,7 @@ import { assert } from 'chai'; import Expression from '../src/util/expression'; -import V from '../src'; +import { clone } from '../src/util/misc'; +import { V, Scope } from '../src'; const UNKNOWN_REF = Object.freeze({ $unknownRefType: 'anything' }); @@ -70,10 +71,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 +120,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 +137,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 +170,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 +196,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 +220,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 deleted file mode 100644 index 9389256..0000000 --- a/test/util/create-shortcuts.js +++ /dev/null @@ -1,48 +0,0 @@ -import { assert } from 'chai'; -import camelcase from 'camelcase'; -import V 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({}) === 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 }), ':('); - }); - 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, ':('); - }); - 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 }), ':('); - }); -}); 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/info.js b/test/util/info.js index f254bea..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 V from '../../src'; +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 f21fcfc..e39187a 100644 --- a/test/util/scope.js +++ b/test/util/scope.js @@ -1,28 +1,31 @@ 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).', () => { +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 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(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 +35,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/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/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); 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)), []); 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' }); }