Commit 6ccb8410 authored by Stefan Cameron's avatar Stefan Cameron

Usability feedback

- Provides direct access to all types and qualifiers right
  off the `rtv` object, such as `rtv.STRING` or `rtv.OPTIONAL`.
- Lifts the restriction on typesets to only have a single
  occurrence of a given type. The same type may now appear
  multiple times in the same typeset, which makes typeset
  composition a lot simpler (e.g. you won't have to a
  custom validator if a value could be the string "foo"
  or a string that begins with "bar").
- Deprecates `rtv.t` in favor of the more self-explanatory
  `rtv.types`.
- Deprecates `rtv.q` in favor of the more self-explanatory
  `rtv.qualifiers`.
- Deprecates `rtv.e` in favor of the more self-explanatory
  `rtv.enabled`.
- `RtvError`: Don't include the typeset in the print output nor the
  serialization since it could be _very_ long and could bloat log
  unnecessarily since the path and cause should be enough to identify
  most issues (and with the path, it's possible to look at the code
  where the full typeset is defined and see what might have gone
  wrong). NOTE: The typeset is still included as a _property_ of
  the error object; this change just affects what's output to the
  console in event an `RtvError` is thrown (and what's output to
  string if you do `error.toString()`).
- If an exception is thrown in `impl`, it's no longer wrapped
  in an "outer" exception, so you won't get an error message like
  `"Cannot check value: Cannot check value: Cannot check value:
  Invalid typeset..."` because the invalid typeset was nested
  3 levels deep. You'll just get `"Invalid typeset..."` now
  (or whatever the exception was).

Also:

- Added `yup` as an alternative (kind of...) in the README.
- Fixed a few typos in the docs here and there.
parent b593ce4f
Pipeline #86823078 passed with stages
in 3 minutes and 29 seconds
......@@ -3,6 +3,9 @@
{
"root": true,
// adds support for things like object/array spread syntax
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
......
This diff is collapsed.
......@@ -9,7 +9,16 @@ Date format is YYYY-MM-DD.
## Unreleased
n/a
### Changed
- All types and qualifiers are now directly accessible on the `rtv` object (e.g. `rtv.STRING`).
- It's now possible to have the same type appear multiple times in the same typeset (this makes typeset _composition_ much easier, avoiding the need for custom validators in lots of cases).
- `RtvError`'s message and string serialization no longer includes the `typeset` to reduce log bloat/noise (they could be _very_ long), but the `typeset` is still available as a property of the object.
- __DEPRECATED__ `rtv.t`, being replaced by `rtv.types`.
- __DEPRECATED__ `rtv.q`, being replaced by `rtv.qualifiers`.
- __DEPRECATED__ `rtv.e`, being replaced by `rtv.enabled`.
### Fixed
- If an exception is thrown in `impl`, it's no longer wrapped in an "outer" exception, resulting in a cleaner error message (e.g. what was once, "Cannot check value: Cannot check value: Invalid typeset..." for an invalid typeset nested 2 levels deep is now just, "Invalid typeset...").
## 2.0.0 - 2019-09-10
......
This diff is collapsed.
......@@ -12,4 +12,7 @@ require('@babel/register');
// @see https://github.com/jsdom/jsdom
// @see https://github.com/airbnb/enzyme/blob/master/docs/guides/jsdom.md
// define global variables that are otherwise defined during the build
global.DEV_ENV = false; // don't run any dev-specific code during tests
module.exports = {}; // nothing for now
......@@ -1379,6 +1379,20 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"optional": true
},
"babel-eslint": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz",
"integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.0.0",
"@babel/traverse": "^7.0.0",
"@babel/types": "^7.0.0",
"eslint-visitor-keys": "^1.0.0",
"resolve": "^1.12.0"
}
},
"babel-plugin-dynamic-import-node": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz",
......@@ -2439,8 +2453,7 @@
"eslint-visitor-keys": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
"optional": true
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A=="
},
"espree": {
"version": "6.1.1",
......@@ -3498,9 +3511,9 @@
"optional": true
},
"handlebars": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz",
"integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz",
"integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
......
......@@ -45,7 +45,7 @@ export default class Enumeration {
throw new Error('map must contain at least one key');
}
// shallow-clone each key in the map into this
// shallow-clone each key in the map into this enumeration
keys.forEach((key) => {
if (key.indexOf('$') === 0) {
throw new Error(`map key "${key}" cannot start with "$"`);
......@@ -68,6 +68,10 @@ export default class Enumeration {
* Friendly name (not necessarily unique among all enumeration instances)
* used to identify this enumeration, especially in validation error
* messages. Empty string if not specified during construction.
*
* Note that this own-property is non-enumerable on purpose. Enumerable
* properties on this instance are the keys in this enumeration.
*
* @readonly
* @name rtvref.Enumeration#$name
* @type {string}
......
......@@ -84,11 +84,12 @@ const RtvError = function(value, typeset, path, cause, failure) {
// NOTE: for security reasons, no part of the value should be included in the
// message in case it contains sensitive information like secrets or passwords
// NOTE: we don't include the `typeset` in the message since it could be VERY long;
// the `path` and `cause` should be enough for debugging purposes
this.message = `Verification failed: path="${renderPath(path)}", cause=${print(cause)}`;
if (failure) {
this.message += `, failure="${failure.message}"`;
}
this.message += `, typeset=${print(typeset)}`;
Object.defineProperties(this, {
/**
......@@ -164,9 +165,9 @@ const RtvError = function(value, typeset, path, cause, failure) {
* the {@link rtvref.RtvError#typeset typeset}, and possibly of a nested
* typeset within it, expressing only the direct cause of the failure.
*
* If `typeset` is `[[rtv.t.STRING]]` (a required array of required strings),
* and `value` is `['a', 2]`, this property would be `[rtv.q.REQUIRED, rtv.t.STRING]`
* because the failure would ultimately have been caused by the nested `rtv.t.STRING`
* If `typeset` is `[[rtv.STRING]]` (a required array of required strings),
* and `value` is `['a', 2]`, this property would be `[rtv.REQUIRED, rtv.STRING]`
* because the failure would ultimately have been caused by the nested `rtv.STRING`
* typeset.
*
* @readonly
......@@ -211,8 +212,10 @@ RtvError.prototype.toString = function() {
// NOTE: for security reasons, no part of the value should be included in the
// serialization in case it contains sensitive information like secrets or
// passwords
// NOTE: we don't include the `typeset` in the serialization since it could be VERY long;
// the `path` and `cause` should be enough for debugging purposes
let str = `{rtvref.RtvError path="${renderPath(this.path)}", cause=${print(this.cause)}}`;
let str = `{rtvref.RtvError path="${renderPath(this.path)}", cause=${print(this.cause)}`;
if (this.failure) {
str += `, failure="${this.failure.message}"`;
......@@ -220,7 +223,7 @@ RtvError.prototype.toString = function() {
str += ', failure=<none>';
}
str += `, typeset=${print(this.typeset)}`;
str += '}';
return str;
};
......
......@@ -690,57 +690,51 @@ const checkWithArray = function(value, array /*, options*/) {
const check = function(value, typeset /*, options*/) {
const options = _getCheckOptions(arguments.length > 2 ? arguments[2] : undefined);
try {
if (options.isTypeset || isTypeset(typeset)) {
options.isTypeset = true;
if (isString(typeset)) {
// simple type: check value is of the type
return checkWithType(value, typeset, options);
}
if (options.isTypeset || isTypeset(typeset)) {
options.isTypeset = true;
if (isCustomValidator(typeset)) {
// custom validator: bare function implies the ANY type
const impliedType = types.ANY;
if (isString(typeset)) {
// simple type: check value is of the type
return checkWithType(value, typeset, options);
}
// value must be ANY type, and custom validator must return true
const result = checkWithType(value, impliedType, options);
if (!result.valid) {
return result;
}
if (isCustomValidator(typeset)) {
// custom validator: bare function implies the ANY type
const impliedType = types.ANY;
// the fully-qualified match should NOT include the validator, only
// the subtype within the implied typeset that matched
const match = fullyQualify(impliedType, options.qualifier);
// value must be ANY type, and custom validator must return true
const result = checkWithType(value, impliedType, options);
if (!result.valid) {
return result;
}
const failure = _callCustomValidator(typeset, value, match, typeset);
if (failure !== undefined) {
return new RtvError(value, typeset, options.path,
fullyQualify(typeset, options.qualifier), failure);
}
// the fully-qualified match should NOT include the validator, only
// the subtype within the implied typeset that matched
const match = fullyQualify(impliedType, options.qualifier);
return new RtvSuccess();
const failure = _callCustomValidator(typeset, value, match, typeset);
if (failure !== undefined) {
return new RtvError(value, typeset, options.path,
fullyQualify(typeset, options.qualifier), failure);
}
if (isShape(typeset)) {
// shape descriptor: check value against shape
return checkWithShape(value, typeset, options);
}
return new RtvSuccess();
}
if (isArray(typeset)) {
// Array typeset: check value against all types in typeset
return checkWithArray(value, typeset, options);
}
if (isShape(typeset)) {
// shape descriptor: check value against shape
return checkWithShape(value, typeset, options);
}
throw new Error(`Invalid JavaScript type for typeset=${print(typeset)}`);
} else {
throw new Error(`Invalid typeset=${print(typeset)} specified`);
if (isArray(typeset)) {
// Array typeset: check value against all types in typeset
return checkWithArray(value, typeset, options);
}
} catch (checkErr) {
const err = new Error(`Cannot check value: ${checkErr.message}`);
err.rootCause = checkErr;
throw err;
throw new Error(`Invalid JavaScript type for typeset=${print(typeset)}`);
}
throw new Error(`Invalid typeset=${print(typeset)}`);
};
/**
......
......@@ -323,9 +323,10 @@ import Enumeration from './Enumeration';
* the typeset. If a simpler type is a more likely match, it's more performant to specify it
* first/earlier in the typeset to avoid a match attempt on a nested shape or Array.
* - Cannot be an empty Array.
* - A given type may not be included more than once in the typeset, but may appear
* again in a nested typeset (when a parent typeset describes an
* {@link rtfref.types.ARRAY Array} or type of {@link rtfref.types.OBJECT Object}).
* - A given type may be included more than once in the typeset. This allows for greater
* composition. The first-matched will win. `[STRING, {oneOf: 'foo'}, STRING]` would
* validate both `'foo'` and `'bar'` because the second occurrence of `STRING` is
* not restricted to any specific value.
* - An Array is necessary to {@link rtvref.qualifiers qualify} the typeset as not
* required (see _Typeset Qualifiers_ below).
* - An Array is necessary if a type needs or requires
......@@ -341,7 +342,7 @@ import Enumeration from './Enumeration';
* these typesets are equivalent (and equivalent to just `{name: STRING}`
* as the typeset): `[{name: STRING}]`, `[REQUIRED, {name: STRING}]`, and
* `[REQUIRED, OBJECT, {$: {name: STRING}}]`, describing an object that has a name
* property which is a non-empty string. Changing it to `[STRING, {$: name: STRING}}]`,
* property which is a non-empty string. Changing it to `[STRING, {$: {name: STRING}}]`,
* however, does __not__ mean, "a non-empty string, or an object with a name
* property which is a non-empty string". In this case, the
* {@link rtvref.types.object_args object arguments} `{$: {name: STRING}}` would
......@@ -391,13 +392,13 @@ import Enumeration from './Enumeration';
* <h4>Typeset Example: Object</h4>
*
* <pre><code>const contactShape = {
* name: rtv.t.STRING, // required, non-empty, string
* tags: [rtv.t.ARRAY, [rtv.t.STRING]], // required array of non-empty strings
* // tags: [[rtv.t.STRING]], // same as above, but using shortcut array format
* name: rtv.STRING, // required, non-empty, string
* tags: [rtv.ARRAY, [rtv.STRING]], // required array of non-empty strings
* // tags: [[rtv.STRING]], // same as above, but using shortcut array format
* details: { // required nested object of type `OBJECT` (default)
* birthday: [rtv.q.EXPECTED, rtv.t.DATE] // Date (could be null)
* birthday: [rtv.EXPECTED, rtv.DATE] // Date (could be null)
* },
* notes: [rtv.q.OPTIONAL, rtv.t.STRING, function(value) { // optional string...
* notes: [rtv.OPTIONAL, rtv.STRING, function(value) { // optional string...
* if (value && !value.test(/^[A-Z].+\.$/)) {
* throw new Error('Note must start with a capital letter, end with a ' +
* period, and have something in between, if specified.');
......@@ -418,10 +419,10 @@ import Enumeration from './Enumeration';
* const walletShape = {
* contacts: [[contactShape]], // list of contacts using nested shape
* address: {
* street: rtv.t.STRING
* street: rtv.STRING
* // ...
* },
* money: rtv.t.FINITE
* money: rtv.FINITE
* };
*
* rtv.verify({
......@@ -433,13 +434,13 @@ import Enumeration from './Enumeration';
*
* <h4>Typeset Example: String</h4>
*
* <pre><code>rtv.verify('foo', rtv.t.STRING); // OK
* rtv.verify('foo', rtv.t.FINITE); // ERROR
* <pre><code>rtv.verify('foo', rtv.STRING); // OK
* rtv.verify('foo', rtv.FINITE); // ERROR
* </code></pre>
*
* <h4>Typeset Example: Array</h4>
*
* <pre><code>const typeset = [rtv.t.STRING, rtv.t.FINITE]; // non-empty string, or finite number
* <pre><code>const typeset = [rtv.STRING, rtv.FINITE]; // non-empty string, or finite number
* rtv.verify('foo', typeset); // OK
* rtv.verify(1, typeset); // OK
* </code></pre>
......@@ -453,14 +454,14 @@ import Enumeration from './Enumeration';
* };
*
* rtv.verify(100, validator); // OK
* rtv.verify(120, [rtv.t.INT, validator]); // OK
* rtv.verify(120, [rtv.INT, validator]); // OK
* </code></pre>
*
* <h4>Typeset Example: Alternate Qualifier</h4>
*
* <pre><code>const person = {
* name: rtv.t.STRING, // required, non-empty
* age: [rtv.q.OPTIONAL, rtv.t.FINITE, (v) => { // 18 or older, if specified
* name: rtv.STRING, // required, non-empty
* age: [rtv.OPTIONAL, rtv.FINITE, (v) => { // 18 or older, if specified
* if (v < 18) {
* throw new Error('Must be 18 or older.');
* }
......@@ -489,7 +490,7 @@ import Enumeration from './Enumeration';
*
* - `STRING` -> `[REQUIRED, STRING]`
* - `{note: STRING}` -> `[REQUIRED, OBJECT, {$: {note: [REQUIRED, STRING]}}]`
* - `[[FINITE]]` -> `[REQUIRED, ARRAY, [REQUIRED, FINITE]]`
* - `[[FINITE]]` -> `[REQUIRED, ARRAY, {ts: [REQUIRED, FINITE]}]`
* - `(v) => if (!v) { throw new Error(); }` -> `[REQUIRED, ANY, (v) => if (!v) { throw new Error(); }]`
*
* @typedef {Array} rtvref.types.fully_qualified_typeset
......
......@@ -81,7 +81,7 @@ export const type = undefined;
* @param {boolean} [options.deep=false] If truthy, deeply-validates any nested
* typesets. Note that typesets in nested shapes are also deeply-validated.
* @param {boolean} [options.fullyQualified=false] If truthy, the typeset must be
* fully-qualified.
* fully-qualified to be valid.
* @param {(string|undefined)} [options.failure] (Output property) If an options
* object is specified, this property will be added and set to a failure message
* IIF the validation fails.
......@@ -107,7 +107,6 @@ export default function isTypeset(v, options = {deep: false, fullyQualified: fal
// must now be an array with at least 2 elements: [qualifier, type]
if (isArray(v) && v.length >= 2) {
const usedTypes = {}; // @type {Object.<string,boolean>} map of simple type to `true`
let curType; // @type {string} current in-scope type
let argType; // @type {(string|undefined)} current in-scope type IIF it accepts args
......@@ -117,14 +116,9 @@ export default function isTypeset(v, options = {deep: false, fullyQualified: fal
const updateCurType = function(newType) {
// set the rule as the current in-scope type
curType = newType;
if (usedTypes[curType]) {
// a type cannot appear more than once in a typeset (but nested is OK)
valid = false;
options.failure = `${failurePrefix}: Type "${curType}" may not be included more than once in the typeset (but may appear again in a nested typeset)`;
}
usedTypes[curType] = true;
// NOTE: there's no restriction on having a type appear only once in a typeset,
// but if there was, this is where we'd catch it, using a map to track which
// types have been used already
};
// iterate through each element in the typeset array to make sure all required
......@@ -217,25 +211,18 @@ export default function isTypeset(v, options = {deep: false, fullyQualified: fal
// definition, and deep (if requested)
} else if (valid && !fullyQualified && isArray(v)) {
const failurePrefix = `Non-qualified ${deep ? 'deep' : 'shallow'} typeset=${print(v)}`;
const usedTypes = {}; // @type {Object.<string,boolean>} map of simple type to `true`
let curType; // @type {string} current in-scope type
let argType; // @type {(string|undefined)} current in-scope type IIF it accepts args
let hasQualifier = false; // true if a qualifier is specified (not implied)
// Updates the current in-scope type (curType) and marks it as used in usedTypes.
// If the type has already been used, it sets valid to false.
// Updates the current in-scope type (curType).
// @param {string} newType New in-scope type.
const updateCurType = function(newType) {
// set the rule as the current in-scope type
curType = newType;
if (usedTypes[curType]) {
// a type cannot appear more than once in a typeset (but nested is OK)
valid = false;
options.failure = `${failurePrefix}: Type "${curType}" may not be included more than once in the typeset (but may appear again in a nested typeset)`;
}
usedTypes[curType] = true;
// NOTE: there's no restriction on having a type appear only once in a typeset,
// but if there was, this is where we'd catch it, using a map to track which
// types have been used already
};
// iterate through each element in the typeset array to make sure all required
......@@ -285,14 +272,13 @@ export default function isTypeset(v, options = {deep: false, fullyQualified: fal
// rule is the type and args all in one, therefore we consume the
// rule/object as the in-scope arg type's arguments
updateCurType(DEFAULT_OBJECT_TYPE);
if (valid) {
soArgs = {$: rule}; // build default args since the rule is the shape itself
valid = (idx === 0 || (hasQualifier && idx === 1));
// NOTE: do not set argType because the shape is the default object type's
// args, so they should be consumed by the in-scope arg type
if (!valid) {
options.failure = `${failurePrefix}: Shape at index=${idx} is missing an object type in ${objTypes}, and should be wrapped in shape object args: Only in the first position (or second if a qualifier is specified) does a shape assume the default object type of "${DEFAULT_OBJECT_TYPE}"`;
}
soArgs = {$: rule}; // build default args since the rule is the shape itself
valid = (idx === 0 || (hasQualifier && idx === 1));
// NOTE: do not set argType because the shape is the default object type's
// args, so they should be consumed by the in-scope arg type
if (!valid) {
options.failure = `${failurePrefix}: Shape at index=${idx} is missing an object type in ${objTypes}, and should be wrapped in shape object args: Only in the first position (or second if a qualifier is specified) does a shape assume the default object type of "${DEFAULT_OBJECT_TYPE}"`;
}
} else {
// consume the object as the in-scope arg type's arguments
......@@ -328,7 +314,7 @@ export default function isTypeset(v, options = {deep: false, fullyQualified: fal
// it from this type as well
argType = undefined;
if (valid && deep) {
if (deep) {
const opts = {deep, fullyQualified};
valid = isTypeset(rule, opts); // recursive
options.failure = opts.failure && `${failurePrefix} (index=${idx}): ${opts.failure}`;
......
......@@ -60,7 +60,7 @@
* {@link rtvref.qualifiers.REQUIRED REQUIRED} to maintain consistent behavior.
* @param {Object} [args] The arguments object, if any/applicable, for the type
* being validated. For example, {@link rtvref.types.STRING_args string args} in
* a typeset such as `[rtv.t.STRING, {min: 5}]` (a required string of at least
* a typeset such as `[rtv.STRING, {min: 5}]` (a required string of at least
* 5 characters in length).
* @returns {(rtvref.RtvSuccess|rtvref.RtvError)} An `RtvSuccess` if valid;
* `RtvError` if not.
......
......@@ -48,24 +48,101 @@ import * as valWeakSet from './lib/validator/valWeakSet';
const rtv = {
/**
* Enumeration of {@link rtvref.types.types types}.
*
* __DEPRECATED__ since version 2.1.0. Please use `rtv.types.<TYPE>`
* or `rtv.<TYPE>` instead.
*
* @deprecated
* @readonly
* @name rtv.t
* @type {rtvref.Enumeration}
*/
get t() {
// DEV_ENV is a global set to false during tests so we have to exclude that
// branch from coverage
/* istanbul ignore next */
if (DEV_ENV) {
// eslint-disable-next-line no-console
console.warn('DEPRECATED in 2.1.0: rtv.t has been deprecated and ' +
'will be removed in the next major release. Please migrate your code to ' +
'use `rtv.types.<TYPE>` or just `rtv.<TYPE>` such as `rtv.STRING`.');
}
return types;
},
/**
* Enumeration of {@link rtvref.types.types types}.
*
* __For convenience, each type is also available directly from this object__,
* e.g. `rtv.STRING`, `rtv.FINITE`, etc.
*
* The Enumeration can be used to perform additional validations (e.g.
* `rtv.types.verify('foo')` would throw because "foo" is not a valid type),
* however whether the type is referenced as `rtv.STRING` or `rtv.types.STRING`
* makes no difference to typeset validation.
*
* @readonly
* @name rtv.types
* @type {rtvref.Enumeration}
*/
get types() {
return types;
},
// also spread ALL enumerable properties of the enumeration into here so that
// we can just call `rtv.STRING` instead of `rtv.types.STRING`
...types,
/**
* Enumeration of {@link rtvref.qualifiers.qualifiers qualifiers}.
*
* __DEPRECATED__ since version 2.1.0. Please use `rtv.qualifiers.<QUALIFIER>`
* or `rtv.<QUALIFIER>` instead.
*
* @deprecated
* @readonly
* @name rtv.q
* @name rtv.t
* @type {rtvref.Enumeration}
*/
get q() {
// DEV_ENV is a global set to false during tests so we have to exclude that
// branch from coverage
/* istanbul ignore next */
if (DEV_ENV) {
// eslint-disable-next-line no-console
console.warn('DEPRECATED in 2.1.0: rtv.q has been deprecated and ' +
'will be removed in the next major release. Please migrate your code to ' +
'use `rtv.qualifiers.<QUALIFIER>` or just `rtv.<QUALIFIER>` such as ' +
'`rtv.EXPECTED`.');
}
return qualifiers;
},
/**
* Enumeration of {@link rtvref.qualifiers.qualifiers qualifiers}.
*
* __For convenience, each qualifier is also available directly from this object__,
* e.g. `rtv.EXPECTED`, `rtv.OPTIONAL`, etc.
*
* The Enumeration can be used to perform additional validations (e.g.
* `rtv.qualifiers.verify('x')` would throw because "x" is not a valid qualifier),
* however whether the qualifier is referenced as `rtv.EXPECTED` or
* `rtv.qualifiers.EXPECTED`` makes no difference to typeset validation.
*
* @readonly
* @name rtv.qualifiers
* @type {rtvref.Enumeration}
*/
get qualifiers() {
return qualifiers;
},
// also spread ALL enumerable properties of the enumeration into here so that
// we can just call `rtv.REQUIRED` instead of `rtv.qualifiers.REQUIRED`
...qualifiers,
/**
* Determines if a value is a typeset.
* @function rtv.isTypeset
......@@ -86,11 +163,35 @@ const rtv = {
/**
* Shortcut proxy for reading {@link rtv.config.enabled}.
*
* __DEPRECATED__ since version 2.1.0. Please use `rtv.enabled` instead.
*
* @deprecated
* @readonly
* @name rtv.e
* @name rtv.enabled
* @type {boolean}
*/
get e() {
// DEV_ENV is a global set to false during tests so we have to exclude that
// branch from coverage
/* istanbul ignore next */
if (DEV_ENV) {
// eslint-disable-next-line no-console
console.warn('DEPRECATED in 2.1.0: rtv.e has been deprecated and ' +
'will be removed in the next major release. Please migrate your code to ' +
'use `rtv.enabled`.');
}
return this.config.enabled;
},
/**
* Shortcut proxy for reading {@link rtv.config.enabled}.
* @readonly
* @name rtv.enabled
* @type {boolean}
*/
get enabled() {
return this.config.enabled;
},
......@@ -116,11 +217,11 @@ const rtv = {
* {@link rtv.verify verify()}, an exception is not thrown__ if the
* `value` is non-compliant.
*
* Since both {@link rtvref.RtvSuccess RtvSuccess}, returned when
* the check succeeds, as well as {@link rtvref.RtvError RtvError}, returned
* when the check fails, have a `valid: boolean` property in common, it's
* Since both {@link rtvref.RtvSuccess RtvSuccess} (returned when
* the check succeeds) as well as {@link rtvref.RtvError RtvError} (returned
* when the check fails) have a `valid: boolean` property in common, it's
* easy to test for success/failure like this:
* `if (rtv.check(2, rtv.t.FINITE).valid) {...}`.
* `if (rtv.check(2, rtv.FINITE).valid) {...}`.
*
* __NOTE:__ This method always returns a success indicator if RTV.js is currently
* {@link rtv.config.enabled disabled}.
......@@ -170,7 +271,7 @@ const rtv = {
}
// NOTE: this method still returns a truthy value so that expressions like
// `rtv.e && rtv.verify(...) && do_something_on_success()` work when this
// `rtv.enabled && rtv.verify(...) && do_something_on_success()` work when this
// method doesn't throw an exception
return new RtvSuccess();
},
......@@ -184,11 +285,11 @@ const rtv = {
* Globally enables or disables {@link rtv.verify} and {@link rtv.check}. When set
* to `false`, these methods are no-ops.
*
* Use this, or the shortcut {@link rtv.e}, to enable code optimization
* Use this, or the shortcut {@link rtv.enabled}, to enable code optimization
* when building source with a bundler that supports _tree shaking_, like
* {@link https://rollupjs.org/ Rollup} or {@link https://webpack.js.org/ Webpack}.
*
* The following plugins can redefine the statement `rtv.e` or `rtv.config.enabled`
* The following plugins can redefine the statement `rtv.enabled` or `rtv.config.enabled`
* as `false` prior to code optimizations that remove unreachable code:
*
* - Rollup: {@link https://github.com/rollup/rollup-plugin-replace rollup-plugin-replace}
......@@ -208,7 +309,7 @@ const rtv = {
* rtv.verify(jsonResult, expectedShape);
* }
*
* rtv.e && rtv.verify(jsonResult, expectedShape); // shorter
* rtv.enabled && rtv.verify(jsonResult, expectedShape); // shorter
*
* ...
* </code></pre>
......@@ -222,7 +323,7 @@ const rtv = {
* plugins: [
* // invoke this plugin _before_ any other plugins
* replacePlugin({
* 'rtv.e': 'false',
* 'rtv.enabled': 'false',
* 'rtv.config.enabled': 'false'
* }),
* ...
......@@ -246,7 +347,7 @@ const rtv = {
return value;
},
set(newValue) {
rtv.verify(newValue, rtv.t.BOOLEAN);
rtv.verify(newValue, rtv.BOOLEAN);
value = newValue;
}
};
......
......@@ -10,7 +10,7 @@ import RtvError from '../src/lib/RtvError';
describe('Integration', function() {
describe('Typesets', function() {
it('should be valid non-fully-qualified typesets', function() {
rtv.isTypeset([rtv.t.BOOLEAN, [rtv.t.STRING], [rtv.t.INT]], {deep: true});
rtv.isTypeset([rtv.BOOLEAN, [rtv.STRING], [rtv.INT]], {deep: true});
});
});
......@@ -30,12 +30,12 @@ describe('Integration', function() {
};
shape = {
title: rtv.t.STRING,
created: [rtv.q.OPTIONAL, rtv.t.DATE],
priority: [rtv.t.INT, {oneOf: [0, 1, 2]}],
note: [rtv.q.EXPECTED, {
text: rtv.t.STRING,
updated: rtv.t.DATE
title: rtv.STRING,
created: [rtv.OPTIONAL, rtv.DATE],
priority: [rtv.INT, {oneOf: [0, 1, 2]}],
note: [rtv.EXPECTED, {
text: rtv.STRING,
updated: rtv.DATE
}]
};
});
......@@ -70,16 +70,16 @@ describe('Integration', function() {
shapes = {
get todo() {
return {
title: rtv.t.STRING,
due: rtv.t.DATE,
priority: [rtv.t.INT, {oneOf: [1, 2, 3, 4]}],
title: rtv.STRING,
due: rtv.DATE,
priority: [rtv.INT, {oneOf: [1, 2, 3, 4]}],
notes: [[this.note]]
};
},
get note() {
return {
text: rtv.t.STRING,
updated: rtv.t.DATE
text: rtv.STRING,
updated: rtv.DATE
};
}
};
......@@ -105,8 +105,8 @@ describe('Integration', function() {
describe('Dynamic Note class', function() {
it('creates a class from a shape with setters that verify values', function() {
const {STRING, DATE} = rtv.t; // some types
const {EXPECTED} = rtv.q; // some qualifiers
const {STRING, DATE} = rtv.types; // some types
const {EXPECTED} = rtv.qualifiers; // some qualifiers
const tags = ['car', 'money', 'reminder', 'grocery'];
const noteShape = {
......
......@@ -3,7 +3,6 @@ import {expect} from 'chai';
import types from '../../src/lib/types';
import qualifiers from '../../src/lib/qualifiers';
import RtvError from '../../src/lib/RtvError';
import {print} from '../../src/lib/util';
describe('module: lib/RtvError', function() {
it('should extend Error', function() {
......@@ -178,7 +177,7 @@ describe('module: lib/RtvError', function() {
expect(err.message).to.contain(` path="/${path.join('/')}"`);
expect(err.message).to.contain(` cause=["${qualifiers.REQUIRED}","${types.STRING}"]`);
expect(err.message).not.to.contain(' failure='); // failure not provided, so no failure message
expect(err.message).to.contain(` typeset=${print(typeset)}`);
expect(err.message).not.to.contain(' typeset='); // typeset not provided, so no typeset message
// for security reasons, should NOT contain the value in case it
// contains sensitive information like passwords
expect(err.message).not.to.contain(' value=');
......@@ -194,7 +193,7 @@ describe('module: lib/RtvError', function() {