Comparing version 7.0.2 to 7.1.0
@@ -1,14 +0,7 @@ | ||
// @flow | ||
const ValueError = require("../errors/ValueError"); | ||
const BlorkError = require("../errors/BlorkError"); | ||
const format = require("../functions/format"); | ||
const modifiers = require("../modifiers"); | ||
const { CLASS, KEYS, VALUES } = require("../constants"); | ||
// Vars. | ||
const R_AND = /\s*&+\s*/; | ||
const R_OR = /\s*\|+\s*/; | ||
const R_INVERT = /^!/; | ||
const R_NONEMPTY = /\+$/; | ||
const R_OPTIONAL = /\?+$/; | ||
/** | ||
@@ -66,2 +59,5 @@ * Check a value with a specified checker. | ||
this._checkers = Object.assign({}, checkers); | ||
// Bind find (we use this a few times). | ||
this._find = this._find.bind(this); | ||
} | ||
@@ -79,3 +75,3 @@ | ||
// Return the checker. | ||
const checker = this._checkers[type]; | ||
const checker = this._checkers[type] || this._lazyCreateChecker(type) || false; | ||
if (checker) return checker; | ||
@@ -259,103 +255,14 @@ | ||
_lazyCreateChecker(type) { | ||
// AND checkers (two string checkers joined with "&") | ||
if (~type.indexOf("&")) { | ||
// Split type and get corresponding checker for each. | ||
const ands = type.split(R_AND).map(this._find, this); | ||
// Loop through modifiers. | ||
for (const i in modifiers) { | ||
// Test the type against the modifier's regex. | ||
const matches = type.match(modifiers[i].regex); | ||
// Check each checker. | ||
const andChecker = value => { | ||
// Loop through and call each checker. | ||
for (const checker of ands) if (!checker(value)) return false; // Fail. | ||
return true; // Otherwise pass. | ||
}; | ||
// Description message joins the descriptions for the checkers. | ||
andChecker.desc = ands.map(checker => checker.desc).join(" and "); | ||
// Add the AND checker to the list of checkers now it has been created. | ||
this._checkers[type] = andChecker; | ||
return andChecker; | ||
// Did the regex match? | ||
if (matches) { | ||
// Yes! Call the modifier's callback to (lazily) create the type. | ||
this._checkers[type] = modifiers[i].callback(matches, this._find); | ||
return this._checkers[type]; // And return it. | ||
} | ||
} | ||
// OR checkers (two string checkers joined with "|") | ||
if (~type.indexOf("|")) { | ||
// Split type and get corresponding checker for each. | ||
const ors = type.split(R_OR).map(this._find, this); | ||
// Check each checker. | ||
const orChecker = value => { | ||
// Loop through and call each checker. | ||
for (const checker of ors) if (checker(value)) return true; // Pass. | ||
return false; // Otherwise fail. | ||
}; | ||
// Description message joins the descriptions for the checkers. | ||
orChecker.desc = ors.map(checker => checker.desc).join(" or "); | ||
// Add the OR checker to the list of checkers now it has been created. | ||
this._checkers[type] = orChecker; | ||
return orChecker; | ||
} | ||
// Inverted value (starts with '!'), e.g. "!num" | ||
if (R_INVERT.test(type)) { | ||
// Find non optional checker (strip '!'). | ||
const checker = this._find(type.replace(R_INVERT, "")); | ||
// Create an optional checker for this optional type. | ||
// Returns 0 if undefined, or passes through to the normal checker. | ||
const invertedChecker = v => !checker(v); | ||
// Description message joins the descriptions for the checkers. | ||
invertedChecker.desc = `not ${checker.desc}`; | ||
// Add the invertedChecker to the list and return it. | ||
this._checkers[type] = invertedChecker; | ||
return invertedChecker; | ||
} | ||
// Non-empty value (ends with '+'), e.g. "str+" | ||
if (R_NONEMPTY.test(type)) { | ||
// Find non optional checker (strip '+'). | ||
const checker = this._find(type.replace(R_NONEMPTY, "")); | ||
// Create a length checker for this optional type. | ||
// Returns true if checker passes and there's a numeric length or size property with a value of >0. | ||
const lengthChecker = v => { | ||
// Must pass the checker. | ||
if (!checker(v)) return false; | ||
// Map and Set use .size | ||
if (v instanceof Map || v instanceof Set) return v.size > 0; | ||
// String and Array use .length | ||
if (typeof v === "string" || v instanceof Array) return v.length > 0; | ||
// Objects use key length. | ||
if (typeof v === "object" && v !== null) return Object.keys(v).length > 0; | ||
// Everything else (numbers, booleans, null, undefined) do a falsy check. | ||
return !!v; | ||
}; | ||
// Description message joins the descriptions for the checkers. | ||
lengthChecker.desc = `non-empty ${checker.desc}`; | ||
// Add the lengthChecker to the list and return it. | ||
this._checkers[type] = lengthChecker; | ||
return lengthChecker; | ||
} | ||
// Optional value (ends with '?'), e.g. "num?" | ||
if (R_OPTIONAL.test(type)) { | ||
// Find non optional checker (strip '?'). | ||
const checker = this._find(type.replace(R_OPTIONAL, "")); | ||
// Create an optional checker for this optional type. | ||
// Returns 0 if undefined, or passes through to the normal checker. | ||
const optionalChecker = v => (v === undefined ? true : checker(v)); | ||
// Description message joins the descriptions for the checkers. | ||
optionalChecker.desc = `${checker.desc} or empty`; | ||
// Add the optionalChecker to the list and return it. | ||
this._checkers[type] = optionalChecker; | ||
return optionalChecker; | ||
} | ||
} | ||
@@ -362,0 +269,0 @@ |
{ | ||
"name": "blork", | ||
"description": "Blork! Mini runtime type checking in Javascript", | ||
"version": "7.0.2", | ||
"version": "7.1.0", | ||
"license": "0BSD", | ||
@@ -6,0 +6,0 @@ "author": "Dave Houlbrooke <dave@shax.com>", |
@@ -457,2 +457,44 @@ # Blork! Mini runtime type checking in Javascript | ||
### String modifiers: Array types | ||
Any string type can be made into an array of that type by appending `[]` brackets to the type reference. This means the check looks for a plain array whose contents only include the specified type. | ||
```js | ||
// Pass. | ||
check(["a", "b"], "str[]"); // No error. | ||
check([1, 2, 3], "int[]"); // No error. | ||
check([], "int[]"); // No error (empty is fine). | ||
check([1], "int[]+"); // No error (non-empty). | ||
// Fail. | ||
check([1, 2], "str[]"); // Throws ValueError "Must be plain array containing only string (received [1, 2])" | ||
check(["a"], "int[]"); // Throws ValueError "Must be plain array containing only integer (received ["a"])" | ||
check([], "int[]+"); // Throws ValueError "Must be non-empty plain array containing only integer (received [])" | ||
``` | ||
### String modifiers: Object types | ||
Check for objects only containing strings of a specified type by surrounding the type in `{}` braces. This means the check looks for a plain object whose contents only include the specified type (whitespace is optional). | ||
```js | ||
// Pass. | ||
check({ a: "a", b: "b" }, "{str}"); // No error. | ||
check({ a: 1, b: 2 }, "{int}"); // No error. | ||
check({}, "{int}"); // No error (empty is fine). | ||
check({ a: 1 }, "{int}+"); // No error (non-empty). | ||
// Fail. | ||
check({ a: 1, b: 2 }, "{str}"); // Throws ValueError "Must be plain object containing only string (received [1, 2])" | ||
check({ a: "a" }, "{int}"); // Throws ValueError "Must be plain object containing only integer (received ["a"])" | ||
check({}, "{int}+"); // Throws ValueError "Must be non-empty plain object containing only integer (received [])" | ||
``` | ||
A type for the keys can also be specified by using `key: value` format. | ||
```js | ||
// Pass. | ||
check({ myVar: 123 }, "{ camel: integer }"); | ||
check({ "my-var": 123 }, "{ kebab: integer }"); | ||
``` | ||
### String modifiers: Optional types | ||
@@ -730,2 +772,4 @@ | ||
- 7.1.0 | ||
- Add object and array string modifiers (using `type[]`, `{type}` and `{ keyType: type }` syntax) | ||
- 7.0.0 | ||
@@ -732,0 +776,0 @@ - Add `VALUES`, `KEYS`, and `CLASS` symbol constants |
@@ -5,2 +5,3 @@ const BlorkError = require("../lib/errors/BlorkError"); | ||
// Tests. | ||
/* eslint-disable prettier/prettier */ | ||
describe("exports.check() string types", () => { | ||
@@ -86,2 +87,41 @@ test("String types pass correctly", () => { | ||
}); | ||
describe("Array types", () => { | ||
test("Array types pass correctly", () => { | ||
expect(check([1, 2, 3], "num[]")).toBe(undefined); | ||
expect(check(["a", "b"], "lower[]")).toBe(undefined); | ||
expect(check([true, false], "bool[]")).toBe(undefined); | ||
}); | ||
test("Array types fail correctly", () => { | ||
expect(() => check(true, "num[]")).toThrow(TypeError); | ||
expect(() => check(false, "num[]")).toThrow(TypeError); | ||
expect(() => check([1, 2, "c"], "num[]")).toThrow(TypeError); | ||
expect(() => check(["a", "b", 3], "str[]")).toThrow(TypeError); | ||
expect(() => check([], "str[]+")).toThrow(TypeError); | ||
expect(() => check(["a", "b", ""], "str+[]")).toThrow(TypeError); | ||
}); | ||
test("Array types have correct error message", () => { | ||
expect(() => check(true, "str[]")).toThrow(/plain array containing only string/); | ||
expect(() => check([], "str[]+")).toThrow(/non-empty plain array containing only string/); | ||
expect(() => check(["a", "b", ""], "str+[]")).toThrow(/plain array containing only non-empty string/); | ||
}); | ||
}); | ||
describe("Object types", () => { | ||
test("Object types pass correctly", () => { | ||
expect(check({ "a": 1, "b": 2, "c": 3 }, "{num}")).toBe(undefined); | ||
expect(check({ "a": "A", "b": "A" }, "{ upper }")).toBe(undefined); | ||
expect(check({ "aaAA": true, "bbBB": false }, "{ camel: bool }")).toBe(undefined); | ||
expect(check({ "aa-aa": true, "bb-bb": false }, "{ slug: bool }")).toBe(undefined); | ||
}); | ||
test("Object types fail correctly", () => { | ||
expect(() => check(true, "{num}")).toThrow(TypeError); | ||
expect(() => check(false, "{num}")).toThrow(TypeError); | ||
expect(() => check([1, 2, 3], "{ num }")).toThrow(TypeError); | ||
expect(() => check({ aaAA: true, bbBB: false }, "{ kebab: bool }")).toThrow(TypeError); | ||
expect(() => check({ "aa-aa": true, "bb-bb": false }, "{ camel: bool }")).toThrow(TypeError); | ||
}); | ||
test("Object types have correct error message", () => { | ||
expect(() => check(true, "{int}")).toThrow(/plain object containing only integer/); | ||
expect(() => check({ "ABC": true }, "{ upper: int }")).toThrow(/plain object with UPPERCASE-only string keys containing only integer/); | ||
}); | ||
}); | ||
describe("Combined types", () => { | ||
@@ -119,16 +159,6 @@ test("AND combined types pass correctly", () => { | ||
test("AND and OR combined types have correct error message", () => { | ||
expect(() => check("ABCdef", "string & lower | upper")).toThrow(/string/); | ||
expect(() => check("ABCdef", "string & lower | upper")).toThrow(/and/); | ||
expect(() => check("ABCdef", "string & lower | upper")).toThrow(/or/); | ||
expect(() => check("ABCdef", "string & lower | upper")).toThrow(/UPPERCASE/); | ||
expect(() => check("ABCdef", "string & lower | upper")).toThrow(/lowercase/); | ||
expect(() => check("ABCdef", "string & lower | upper")).toThrow(/string/); | ||
expect(() => check("ABCdef", "lower | upper & string")).toThrow(/string/); | ||
expect(() => check("ABCdef", "lower | upper & string")).toThrow(/and/); | ||
expect(() => check("ABCdef", "lower | upper & string")).toThrow(/or/); | ||
expect(() => check("ABCdef", "lower | upper & string")).toThrow(/UPPERCASE/); | ||
expect(() => check("ABCdef", "lower | upper & string")).toThrow(/lowercase/); | ||
expect(() => check("ABCdef", "lower | upper & string")).toThrow(/string/); | ||
expect(() => check(1, "string & string | string")).toThrow(/string and string or string/); | ||
expect(() => check(1, "string | string & string")).toThrow(/string or string and string/); | ||
}); | ||
}); | ||
}); |
141236
40
2363
814