Initial commit

master
Sven Slootweg 6 years ago
commit 98fbf1d556

@ -0,0 +1,69 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
/* Things that should effectively be syntax errors. */
"indent": [ "error", "tab", {
SwitchCase: 1
}],
"linebreak-style": [ "error", "unix" ],
"semi": [ "error", "always" ],
/* Things that are always mistakes. */
"getter-return": [ "error" ],
"no-compare-neg-zero": [ "error" ],
"no-dupe-args": [ "error" ],
"no-dupe-keys": [ "error" ],
"no-duplicate-case": [ "error" ],
"no-empty": [ "error" ],
"no-empty-character-class": [ "error" ],
"no-ex-assign": [ "error" ],
"no-extra-semi": [ "error" ],
"no-func-assign": [ "error" ],
"no-invalid-regexp": [ "error" ],
"no-irregular-whitespace": [ "error" ],
"no-obj-calls": [ "error" ],
"no-sparse-arrays": [ "error" ],
"no-undef": [ "error" ],
"no-unreachable": [ "error" ],
"no-unsafe-finally": [ "error" ],
"use-isnan": [ "error" ],
"valid-typeof": [ "error" ],
"curly": [ "error" ],
"no-caller": [ "error" ],
"no-fallthrough": [ "error" ],
"no-extra-bind": [ "error" ],
"no-extra-label": [ "error" ],
"array-callback-return": [ "error" ],
"prefer-promise-reject-errors": [ "error" ],
"no-with": [ "error" ],
"no-useless-concat": [ "error" ],
"no-unused-labels": [ "error" ],
"no-unused-expressions": [ "error" ],
"no-unused-vars": [ "error", { argsIgnorePattern: "^_" } ],
"no-return-assign": [ "error" ],
"no-self-assign": [ "error" ],
"no-new-wrappers": [ "error" ],
"no-redeclare": [ "error" ],
"no-loop-func": [ "error" ],
"no-implicit-globals": [ "error" ],
"strict": [ "error", "global" ],
/* Development code that should be removed before deployment. */
"no-console": [ "warn" ],
"no-constant-condition": [ "warn" ],
"no-debugger": [ "warn" ],
"no-alert": [ "warn" ],
"no-warning-comments": ["warn", {
terms: ["fixme"]
}],
/* Common mistakes that can *occasionally* be intentional. */
"no-template-curly-in-string": ["warn"],
"no-unsafe-negation": [ "warn" ],
}
};

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,3 @@
"use strict";
module.exports = require("./src");

@ -0,0 +1,39 @@
"use strict";
const benchmark = require("benchmark");
const s = require("../src");
let PersonName = s.createType("PersonName", s.string({
matches: /^[a-z0-9 '-]+$/
}));
let Person = s.createType("Person", {
name: PersonName,
age: s.number(),
isAlive: s.boolean()
});
new benchmark.Suite()
.add("normal", () => {
let me = {
name: "somebody",
age: 42,
isAlive: true
};
let a = (me.name, me.age, me.isAlive);
})
.add("strict", () => {
let me = Person({
name: "somebody",
age: 42,
isAlive: true
});
let a = (me.name, me.age, me.isAlive);
})
.on("cycle", function (event) {
console.log(String(event.target));
})
.run();

@ -0,0 +1,118 @@
"use strict";
const benchmark = require("benchmark");
const dm = require("../src");
let Identity = dm.createType("Identity", {
label: dm.string(),
nickname: dm.string({
matches: /^[a-z0-9_-]+$/
}),
emailAddress: dm.string({
matches: /@/
}),
xmppAddress: dm.string({
matches: /@/
}).optional(),
notes: dm.string().optional()
});
let User = dm.createType("User", {
name: dm.string(),
isActive: dm.boolean(),
registrationDate: dm.instanceOf(Date),
misc: dm.either(
dm.string(),
dm.number()
),
// undefinedValue: dm.undefined(),
// nullValue: dm.null(),
// nothingValue: dm.nothing(),
age: dm.number({
minimum: 0
}),
luckyNumbers: dm.setOf(dm.number({
minimum: 1,
maximum: 42
})),
identities: dm.setOf(Identity),
mainIdentity: Identity,
codeWords: dm.mapOf(dm.string(), dm.string())
});
new benchmark.Suite()
.add("normal", () => {
let me = {
name: "Sven Slootweg",
isActive: true,
registrationDate: new Date(),
age: 26,
luckyNumbers: new Set([13, 42]),
misc: 42,
identities: new Set([
{
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
},
{
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net"
}
]),
mainIdentity: {
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
},
codeWords: new Map([
["foo", "Foo"],
["bar", "Bar"]
])
};
})
.add("strict", () => {
let me = User({
name: "Sven Slootweg",
isActive: true,
registrationDate: new Date(),
age: 26,
luckyNumbers: new Set([13, 42]),
misc: 42,
identities: new Set([
Identity({
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}),
Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net"
})
]),
mainIdentity: Identity({
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}),
codeWords: new Map([
["foo", "Foo"],
["bar", "Bar"]
])
});
})
.on("cycle", function (event) {
console.log(String(event.target));
})
.run();

@ -0,0 +1,26 @@
"use strict";
const s = require("../");
let PersonName = s.createType(s.string({
matches: /^[a-z0-9 '-]+$/
}));
let Person = s.createType({
name: PersonName,
age: s.number(),
isAlive: s.boolean()
});
for (let i = 0; i < 500000; i++) {
let me = Person({
name: "somebody",
age: 42,
isAlive: true
});
let a = (me.name, me.age, me.isAlive);
}
console.log("Done");

@ -0,0 +1,147 @@
"use strict";
const s = require("../");
let PersonName = s.createType("PersonName", s.string({
matches: /^[a-z0-9 '-]+$/
}));
let Nickname = s.createType("Nickname", s.string({
matches: /^[a-z0-9_]+$/
}));
let EmailAddress = s.createType("EmailAddress", s.string({
matches: /@/
}));
// let OnlineIdentity = s.createType("OnlineIdentity", {
// nickname: Nickname,
// active: s.boolean()
// });
let OnlineIdentity = s.createTrait("OnlineIdentity", {
encode: s.function([], s.string())
});
let NicknameIdentity = s.createType("NicknameIdentity", {
nickname: Nickname
}).implements(OnlineIdentity, {
encode: s.guard([], s.string(), function () {
return `nickname:${this.nickname}`;
})
});
let EmailIdentity = s.createType("EmailIdentity", {
email: EmailAddress
}).implements(OnlineIdentity, {
encode: s.guard([], s.string(), function () {
return `email:${this.email}`;
})
});
let CustomIdentity = s.createType("CustomIdentity", {
}).implements(OnlineIdentity, {
encode: s.slot()
});
let BullshitIdentity = s.createType("BullshitIdentity", {
nickname: Nickname
});
let Person = s.createType("Person", {
name: s.string(),
age: s.number(),
isAlive: s.boolean(),
// identities: s.setOf(OnlineIdentity)
identities: s.mapOf(s.string(), OnlineIdentity),
// friend: s.self().optional(),
friends: s.setOf(s.self()),
getName: function () {
return this.name;
},
compareName: s.guard([s.self()], s.boolean(), function (otherPerson) {
return (otherPerson.name === this.name);
})
});
// MARKER: implement unique paths
let me = Person({
name: "somebody",
age: 42,
isAlive: true,
// identities: new Set([
// OnlineIdentity({
// nickname: "foo",
// active: true
// }),
// OnlineIdentity({
// nickname: "joepie",
// active: false
// })
// ])
friends: new Set(),
identities: new Map([
["foo", EmailIdentity({
email: "foo@bar.baz"
})],
["joepie91", NicknameIdentity({
nickname: "joepie91"
})],
["custom_man", CustomIdentity({
encode: s.guard([], s.string(), function () {
return "custom:foo";
})
})],
])
});
let other = Person({
name: "foo",
age: 21,
isAlive: false,
friends: new Set(),
identities: new Map(),
// friend: me
});
console.log(me.getName(), me.age, me.isAlive, me.identities);
console.log("=====");
for (let [name, identity] of me.identities.entries()) {
console.log(`${name} => ${identity.encode()}`);
}
console.log("=====");
console.log(me.compareName(other));
// let C = s.createType("C", {});
//
// let B = s.createType("B", {
// c: s.setOf(C)
// });
//
// let A = s.createType("A", {
// b: s.mapOf(s.string(), B)
// });
//
// let a = A({
// b: new Map([
// ["1", B({
// c: new Set([
// C({}),
// C({})
// ])
// })],
// ["2", B({
// c: new Set()
// })]
// ])
// });
//
// let a2 = A({
// b: a.b.get("1").c
// // b: "foo"
// });
//
// console.log(a);
// console.log(a2);

296
package-lock.json generated

@ -0,0 +1,296 @@
{
"name": "node-data-modeling",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"dev": true,
"requires": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
"capitalize": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/capitalize/-/capitalize-1.0.0.tgz",
"integrity": "sha1-3IAsWAruEBkpAg0soUtMqKCuRL4="
},
"chai": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
"integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
"dev": true,
"requires": {
"assertion-error": "^1.0.1",
"check-error": "^1.0.1",
"deep-eql": "^3.0.0",
"get-func-name": "^2.0.0",
"pathval": "^1.0.0",
"type-detect": "^4.0.0"
}
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"create-error": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/create-error/-/create-error-0.3.1.tgz",
"integrity": "sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM="
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
}
},
"diff": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
"dev": true
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"he": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"lodash": {
"version": "4.17.10",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz",
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==",
"dev": true
},
"map-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz",
"integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"mocha": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
"integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
"dev": true,
"requires": {
"browser-stdout": "1.3.1",
"commander": "2.15.1",
"debug": "3.1.0",
"diff": "3.5.0",
"escape-string-regexp": "1.0.5",
"glob": "7.1.2",
"growl": "1.10.5",
"he": "1.1.1",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"supports-color": "5.4.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"object-hash": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.0.tgz",
"integrity": "sha512-05KzQ70lSeGSrZJQXE5wNDiTkBJDlUT/myi6RX9dVIvz7a7Qh4oH93BQdiPMn27nldYvVQCKMUaM83AfizZlsQ=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"pathval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"platform": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz",
"integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==",
"dev": true
},
"supports-color": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

@ -0,0 +1,35 @@
{
"name": "node-data-modeling",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "mocha"
},
"repository": {
"type": "git",
"url": "git@git.cryto.net:joepie91/node-data-modeling.git"
},
"keywords": [
"data",
"modeling",
"schema",
"ddl",
"typing",
"validation"
],
"author": "Sven Slootweg <admin@cryto.net>",
"license": "WTFPL",
"devDependencies": {
"benchmark": "^2.1.4",
"chai": "^4.1.2",
"mocha": "^5.2.0"
},
"dependencies": {
"capitalize": "^1.0.0",
"create-error": "^0.3.1",
"filter-obj": "^1.1.0",
"map-obj": "^2.0.0",
"object-hash": "^1.3.0"
}
}

@ -0,0 +1,61 @@
"use strict";
const mapObj = require("map-obj");
const filterObj = require("filter-obj");
const typeRules = require("./type-rules");
const generateDescriptor = require("./generate-descriptor");
const getSchemaKeys = require("./util/get-schema-keys");
const nullMissingFields = require("./util/null-missing-fields");
/* TODO: Traits implementing traits */
module.exports = function createTrait(name, schema) {
let schemaDescriptors = mapObj(schema, (key, rule) => {
return generateDescriptor(rule, key, true);
});
let schemaKeys = getSchemaKeys(schema);
return typeRules._createTypeRule({
_isTrait: true,
_name: name,
applyImplementation: function (implementation) {
/* FIXME: Verify that there are no extraneous keys or unspecified values */
let validatedImplementation = Object.create({
_data: {}
}, schemaDescriptors);
let implementationWithoutSlots = filterObj(implementation, (key, value) => {
return (value == null || value._isSlotRule !== true);
});
let nullFields = nullMissingFields(implementation, schemaKeys);
Object.assign(validatedImplementation, implementationWithoutSlots, nullFields);
// let slotProperties = filterObj(implementation, (key, value) => {
// return (value != null && value._isSlotRule === true);
// });
//
// let slotDescriptors = mapObj(slotProperties, (key, value) => {
// return generateDescriptor(schema[key], key);
// });
let implementationDescriptors = mapObj(implementation, (key, value) => {
if (value != null && value._isSlotRule === true) {
return generateDescriptor(schema[key], key, true);
} else {
return generateDescriptor(value, key);
}
});
/* FIXME: Get rid of duplication here somehow? */
let slotSchemaKeys = Object.keys(implementation).filter((key) => {
let value = implementation[key];
return (value != null && value._isSlotRule != null);
});
return {implementationDescriptors, slotSchemaKeys};
}
});
};

@ -0,0 +1,95 @@
"use strict";
const objectHash = require("object-hash");
const mapObj = require("map-obj");
const typeRules = require("./type-rules");
const generateDescriptor = require("./generate-descriptor");
const getSchemaKeys = require("./util/get-schema-keys");
const nullMissingFields = require("./util/null-missing-fields");
module.exports = function createType(name, schema) {
if (schema._isTypeRule === true) {
return typeRules._createTypeRule({
_typeName: name,
_isTypeAlias: true,
_alias: schema
});
} else {
let propertyDescriptors = mapObj(schema, (key, rule) => {
return generateDescriptor(rule, key);
});
let protoProperties = {
_typeName: name,
/* We pre-define this here so that instances can be monomorphic; this provides a 4x performance improvement. */
_data: mapObj(schema, (key, _value) => {
return [key, undefined];
})
};
let proto = Object.create(protoProperties, propertyDescriptors);
let schemaKeys = getSchemaKeys(schema);
let factory = function createInstance(instanceProps) {
factory._isUsed = true;
let instance = Object.create(proto);
instance._data = {};
// for (let key of factory._schemaKeys) {
// if (instanceProps[key] != null) {
// instance[key] = instanceProps[key];
// } else {
// instance[key] = null;
// }
// }
/* FIXME: Investigate ways to optimize this */
let nullFields = nullMissingFields(instanceProps, factory._schemaKeys);
Object.assign(instance, instanceProps, nullFields);
return instance;
};
proto._type = factory;
factory._schemaKeys = schemaKeys;
factory._isCustomType = true;
factory._name = name;
factory.prototype = proto;
factory.description = name;
factory._createValidator = function () {
return function (value) {
return (value instanceof factory);
};
};
proto._selfValidator = factory._createValidator();
factory._isUsed = false;
factory._implementedTraits = new Set();
factory.implements = function (trait, implementation) {
if (factory._isUsed !== false) {
throw new Error("Cannot modify a custom type that has already been instantiated");
} else {
let {implementationDescriptors, slotSchemaKeys} = trait.applyImplementation(implementation);
// FIXME: Test this
Object.keys(implementationDescriptors).forEach((key) => {
if (proto.key != null) {
throw new Error(`Trait implementation attempted to define a property '${key}' on the '${name}' type, but that property already exists`);
}
});
Object.defineProperties(proto, implementationDescriptors);
factory._implementedTraits.add(trait);
factory._schemaKeys = factory._schemaKeys.concat(slotSchemaKeys);
return this;
}
};
return typeRules._createTypeRule(factory);
}
};

@ -0,0 +1,7 @@
"use strict";
const createError = require("create-error");
module.exports = {
ValidationError: createError("ValidationError")
};

@ -0,0 +1,79 @@
"use strict";
const generateValidator = require("./generate-validator");
const guardedSet = require("./guarded-collections/set");
const guardedMap = require("./guarded-collections/map");
module.exports = function generateDescriptor(rule, key, allowSlot = false) {
if (rule._isTypeRule === true) {
let validator = generateValidator(rule, key);
if (rule._collectionType != null) {
let guardedCollectionFactory;
if (rule._collectionType === "set") {
guardedCollectionFactory = guardedSet;
} else if (rule._collectionType === "map") {
guardedCollectionFactory = guardedMap;
} else {
throw new Error(`Unknown collection type: ${rule._collectionType}`);
}
let valueGuard = generateValidator(rule._itemType);
let keyGuard = (rule._keyType != null) ? generateValidator(rule._keyType) : null;
return [key, {
enumerable: true,
get: function () {
return this._data[key];
},
set: function (value) {
if (allowSlot && value != null && value._isSlotRule === true) {
this._data[key] = value;
} else {
let validationResult = validator.call(this, value);
if (validationResult === true) {
if (value == null) {
this._data[key] = value;
} else {
this._data[key] = guardedCollectionFactory(value, valueGuard, keyGuard, this);
}
} else if (validationResult._default !== undefined) {
this._data[key] = guardedCollectionFactory(validationResult._default, valueGuard, keyGuard, this);
} else {
throw validationResult;
}
}
}
}];
} else {
return [key, {
enumerable: true,
get: function () {
return this._data[key];
},
set: function (value) {
if (allowSlot && value != null && value._isSlotRule === true) {
this._data[key] = value;
} else {
let validationResult = validator.call(this, value);
if (validationResult === true) {
this._data[key] = value;
} else if (validationResult._default !== undefined) {
this._data[key] = validationResult._default;
} else {
throw validationResult;
}
}
}
}];
}
} else {
/* TODO: Does this cause a slowdown compared to merging in these props with Object.assign later? */
return [key, {
value: rule
}];
}
};

@ -0,0 +1,170 @@
"use strict";
const util = require("util");
const createStringValidator = require("./validator-functions/string");
const createNumberValidator = require("./validator-functions/number");
const createBooleanValidator = require("./validator-functions/boolean");
const createFunctionValidator = require("./validator-functions/function");
const createNothingValidator = require("./validator-functions/nothing");
const createNullValidator = require("./validator-functions/null");
const createUndefinedValidator = require("./validator-functions/undefined");
const getValueType = require("./util/get-value-type");
const errors = require("./errors");
module.exports = function generateValidator(rule, name = "<unknown>") {
let baseRule;
if (rule._baseType != null) {
let validatorFactories = {
string: createStringValidator,
boolean: createBooleanValidator,
number: createNumberValidator,
guardedFunction: createFunctionValidator,
nothing: createNothingValidator,
null: createNullValidator,
undefined: createUndefinedValidator
};
let factory = validatorFactories[rule._baseType];
if (factory != null) {
baseRule = factory(rule._options, name);
} else {
throw new Error(`Unrecognized base type: ${rule._baseType}`);
}
} else if (rule._collectionType != null) {
baseRule = () => true; /* FIXME */
} else if (rule._modifierType != null) {
if (rule._modifierType === "either") {
let validators = rule._types.map((type) => generateValidator(type));
baseRule = function (value) {
let matched = false;
for (let validator of validators) {
if (validator.call(this, value) === true) {
matched = true;
break;
}
}
if (matched === true) {
return true;
} else {
let acceptableTypes = rule._types.map((type) => getValueType(type, false)).join(", ");
return new errors.ValidationError(`Expected one of (${acceptableTypes}), got ${getValueType(value)} instead`);
}
};
} else {
throw new Error(`Unrecognized modifier: ${rule._modifierType}`);
}
} else if (rule._isTypeAlias === true) {
baseRule = generateValidator(rule._alias, name);
} else if (rule._isCustomType === true) {
let validator = rule._createValidator();
baseRule = function (value) {
if (validator.call(this, value) === true) {
return true;
} else {
return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`);
}
};
} else if (rule._isTrait === true) {
baseRule = function (value) {
if (value._type != null && value._type._implementedTraits != null && value._type._implementedTraits.has(rule)) {
return true;
} else {
return new errors.ValidationError(`Expected object of a type with the ${rule._name} trait, got ${getValueType(value)} instead`);
}
};
} else if (rule._isSelfRule === true) {
baseRule = function (value) {
if (value instanceof this._type) {
return true;
} else {
return new errors.ValidationError(`Expected ${getValueType(this._type)}, got ${getValueType(value)} instead`);
}
};
} else if (rule._constructorType != null) {
/* This is for handling third-party constructors and their instances. */
baseRule = function (value) {
if (value instanceof rule._constructorType) {
return true;
} else {
return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`);
}
};
} else {
throw new Error(`Unrecognized rule type for rule ${util.inspect(rule)}`); /* FIXME? */
}
let compositeFunction;
if (rule._constraints.length === 0) {
compositeFunction = function baseRuleWrapper(value) {
if (value === undefined && rule._allowUndefined === true) {
return true;
} else if (value == null) {
return new errors.ValidationError(`Value is required for property '${name}'`, {
property: name
});
} else {
return baseRule.call(this, value);
}
};
} else {
let hasDefaultValue = false, defaultValue;
let filteredConstraints = rule._constraints.filter((constraint) => {
if (constraint.type === "default") {
hasDefaultValue = true;
defaultValue = constraint.value;
return false;
} else {
return true;
}
});
let rules = [baseRule].concat(filteredConstraints.map((constraint) => {
if (constraint.type === "validate") {
return constraint.validator;
} else {
throw new Error(`Encountered unrecognized constraint type: ${constraint.type}`);
}
}));
compositeFunction = function complexRuleWrapper(value) {
if (value == null) {
if (hasDefaultValue) {
return {
_default: defaultValue
};
} else if (value === undefined && rule._allowUndefined === true) {
return true;
} else {
return new errors.ValidationError(`Value is required for property '${name}'`, {
property: name
});
}
} else {
/* FIXME: Possibly special-case (for better performance) if the only extra rule is a 'default value' rule? This would avoid a `for` loop in the case where a value is explicitly specified. */
for (rule of rules) {
let result = rule.call(this, value);
if (result === true) {
continue;
} else {
return result;
}
}
return true;
}
};
}
compositeFunction._rule = rule;
return compositeFunction;
};

@ -0,0 +1,45 @@
"use strict";
const generateValidator = require("./generate-validator");
module.exports = function guardFunction(args, returnType, func) {
let rules = args.map((arg) => {
return generateValidator(arg);
});
let returnValueValidator = generateValidator(returnType);
let guardedFunction = function (...params) {
let paramsWithDefaults = new Array(params.length);
/* TODO: Can this be made faster by manually incrementing the index outside of the loop, or even by using forEach? ref https://jsperf.com/for-of-vs-foreach-with-index */
for (let [i, rule] of rules.entries()) {
let validatorResult = rule.call(this, params[i]);
if (validatorResult === true) {
paramsWithDefaults[i] = params[i];
} else if (validatorResult._default !== undefined) {
paramsWithDefaults[i] = validatorResult._default;
} else {
throw validatorResult;
}
}
let returnValue = func.apply(this, paramsWithDefaults);
let returnValueValidatorResult = returnValueValidator.call(this, returnValue);
if (returnValueValidatorResult === true) {
return returnValue;
} else if (returnValueValidatorResult._default !== undefined) {
return returnValueValidatorResult.default;
} else {
throw returnValueValidatorResult;
}
};
guardedFunction._guardedArgs = args;
guardedFunction._guardedReturnType = returnType;
guardedFunction._guardedFunction = func;
return guardedFunction;
};

@ -0,0 +1,92 @@
"use strict";
const util = require("util");
const errors = require("../errors");
const createWrapper = require("../util/function-wrapper");
const getValueType = require("../util/get-value-type");
let proto = {
_source: null,
set: null,
size: 0,
[Symbol.iterator]: createWrapper(Symbol.iterator),
[util.inspect.custom]: function () {
return this.valueOf();
},
clear: function () {
this._source.clear();
this.size = 0;
},
entries: createWrapper("entries"),
forEach: createWrapper("forEach"),
get: createWrapper("get"),
has: createWrapper("has"),
keys: createWrapper("keys"),
values: createWrapper("values"),
toString: createWrapper("toString"),
valueOf: createWrapper("valueOf"),
delete: function (key) {
let deleted = this._source.delete(key);
this.size = this._source.size;
if (deleted === false) {
throw new errors.ValidationError("Tried to delete non-existent key from guarded map", {
key: key,
nap: this._source
});
}
}
};
module.exports = function createGuardedMap(map, valueGuard, keyGuard, parent) {
function generateMapSignatureError() {
let wantedSignature = {
_guardedCollectionType: "map",
_keyType: keyGuard._rule,
_itemType: valueGuard._rule
};
return new errors.ValidationError(`Expected a Map or ${getValueType(wantedSignature)}, got ${getValueType(map)} instead`);
}
if (map._guardedCollectionType === "map") {
if (keyGuard === map._keyType && valueGuard === map._itemType) {
return map;
} else {
throw generateMapSignatureError();
}
} else if (map instanceof Map) {
function check(key, value) {
let keyGuardResult = keyGuard.call(parent, key);
let valueGuardResult = valueGuard.call(parent, value);
if (keyGuardResult === true && valueGuardResult === true) {
return true;
} else if (keyGuardResult !== true) {
throw keyGuardResult;
} else {
throw valueGuardResult;
}
}
for (let [key, value] of map.entries()) {
check(key, value);
}
return Object.assign(Object.create(proto), {
_source: map,
_guardedCollectionType: "map",
_itemType: valueGuard,
_keyType: keyGuard,
set: function (key, value) {
if (check(key, value) === true) {
this._source.set(key, value);
this.size = this._source.size;
}
}
});
} else {
throw generateMapSignatureError();
}
};

@ -0,0 +1,87 @@
"use strict";
const util = require("util");
const errors = require("../errors");
const createWrapper = require("../util/function-wrapper");
const getValueType = require("../util/get-value-type");
let proto = {
_source: null,
description: null,
size: 0,
add: null,
[Symbol.iterator]: createWrapper(Symbol.iterator),
[util.inspect.custom]: function () {
return this.valueOf();
},
clear: function () {
this._source.clear();
this.size = 0;
},
entries: createWrapper("entries"),
forEach: createWrapper("forEach"), /* FIXME: Prevent mutation from the third argument to forEach (which is the original set); maybe also needed for Map? */
has: createWrapper("has"),
keys: createWrapper("keys"),
values: createWrapper("values"),
toString: createWrapper("toString"),
valueOf: createWrapper("valueOf"),
delete: function (value) {
let deleted = this._source.delete(value);
this.size = this._source.size;
if (deleted === false) {
throw new errors.ValidationError("Tried to delete non-existent value from guarded set", {
value: value,
set: this._source
});
}
}
};
module.exports = function createGuardedSet(set, guard, _, parent) {
function generateSetSignatureError() {
let wantedSignature = {
_guardedCollectionType: "set",
_itemType: guard._rule
};
return new errors.ValidationError(`Expected a Set or ${getValueType(wantedSignature)}, got ${getValueType(set)} instead`);
}
if (set._guardedCollectionType === "set") {
if (guard === set._itemType) {
return set;
} else {
throw generateSetSignatureError();
}
} else if (set instanceof Set) {
function check(value) {
let guardResult = guard.call(parent, value);
if (guardResult === true) {
return true;
} else {
throw guardResult;
}
}
for (let value of set.values()) {
check(value);
}
return Object.assign(Object.create(proto), {
_source: set,
_guardedCollectionType: "set",
_itemType: guard,
add: function (value) {
if (check(value) === true) {
this._source.add(value);
this.size = this._source.size;
}
}
});
} else {
throw generateSetSignatureError();
}
};

@ -0,0 +1,31 @@
"use strict";
const typeRules = require("./type-rules");
const createType = require("./create-type");
const createTrait = require("./create-trait");
const guardFunction = require("./guard-function");
const errors = require("./errors");
/* Traits:
- Special 'slot' value; for use in trait implementations to indicate that the specified trait definition property should be filled in by the instance constructor, not by the trait implementation
Possible field types for a trait:
- Static value [add the value to the type's prototype?]
- Type/validation rule [check against implementation, see below]
Possible field types for a trait implementation (all of them fill in type/validation rules):
- Static value, including an appropriately guarded function [add the value to the type's prototype?]
- Slot [add the trait rule to the type rules]
Disallow extra fields in trait implementations!
TODO: Add full property paths to errors?
*/
module.exports = Object.assign({
createType: createType,
createTrait: createTrait,
guard: guardFunction,
ValidationError: errors.ValidationError
}, typeRules);

@ -0,0 +1,115 @@
"use strict";
/* TODO: Implement .unique */
function createTypeRule(props) {
return Object.assign(props, {
_isTypeRule: true,
_constraints: [],
validate: function (validator) {
this._constraints.push({
type: "validate",
validator: validator
});
return this;
},
optional: function () {
return this.default(null);
},
default: function (value) {
this._constraints.push({
type: "default",
value: value
});
return this;
}
});
}
function createBaseTypeFunction(typeName, allowUndefined = false) {
return function (options = {}) {
return createTypeRule({
_baseType: typeName,
_options: options,
_allowUndefined: allowUndefined
});
};
}
function createCollectionTypeFunction(typeName, hasKeyType) {
if (hasKeyType) {
return function (keyType, itemType, options = {}) {
if (keyType == null || itemType == null) {
throw new Error("Must specify both a key type and a value type");
}
return createTypeRule({
_collectionType: typeName,
_itemType: itemType,
_keyType: keyType,
_options: options
});
};
} else {
return function (itemType, options = {}) {
return createTypeRule({
_collectionType: typeName,
_itemType: itemType,
_options: options
});
};
}
}
module.exports = {
_createTypeRule: createTypeRule,
string: createBaseTypeFunction("string"),
boolean: createBaseTypeFunction("boolean"),
number: createBaseTypeFunction("number"),
nothing: createBaseTypeFunction("nothing", true),
null: createBaseTypeFunction("null"),
undefined: createBaseTypeFunction("undefined", true),
instanceOf: function (constructor) {
/* TEST: Check that constructor is actually a function */
return createTypeRule({
_constructorType: constructor
});
},
setOf: createCollectionTypeFunction("set", false),
arrayOf: createCollectionTypeFunction("array", false),
mapOf: createCollectionTypeFunction("map", true),
/* TODO: Consider whether objectOf is needed/desirable at all */
// objectOf: createCollectionTypeFunction("map", true),
function: function (args, returnType) {
if (args == null) {
throw new Error("Must specify argument types when creating a function rule");
} else if (returnType == null) {
throw new Error("Must specify a return type when creating a function rule");
}
return createTypeRule({
_baseType: "guardedFunction",
_options: {
args: args,
returnType: returnType
}
});
},
slot: function () {
return createTypeRule({
_isSlotRule: true
});
},
self: function () {
return createTypeRule({
_isSelfRule: true
});
},
either: function (...types) {
return createTypeRule({
_modifierType: "either",
_types: types
});
}
};

@ -0,0 +1,7 @@
"use strict";
module.exports = function createWrapper(func) {
return function (...params) {
return this._source[func](...params);
};
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function getSchemaKeys(schema) {
return Object.keys(schema).filter((key) => (schema[key]._isTypeRule === true));
};

@ -0,0 +1,183 @@
"use strict";
const util = require("util");
const capitalize = require("capitalize");
const isConstructor = require("./is-constructor");
const isNamedFunction = require("./is-named-function");
/* TODO: Add property tracking throughout calls, to determine exactly where the wrong value was specified. */
module.exports = getValueType;
function getValueType(value, withArticle = true) {
let [article, description] = getValueTypeData(value);
if (withArticle) {
if (article != null) {
return `${article} ${description}`;
} else {
return description;
}
} else {
return description;
}
}
function getValueTypeData(value) {
if (value != null && value._isTypeRule) {
/*
_baseType
boolean
string
number
nothing?
null
undefined
function?
guardedFunction
_collectionType
set
map
array
_modifierType
either
_constructorType
(for instanceOf)
_isSlotRule
_isSelfRule
custom type
*/
if (value._isSlotRule) {
return [null, "(slot)"];
} else if (value._isSelfRule) {
return [null, "(self)"];
} else if (value._constructorType != null) {
let constructorName;
if (value._constructorType.name != null) {
constructorName = value._constructorType.name;
} else {
constructorName = "(unnamed type)";
}
return ["an", `instance of ${constructorName}`];
} else if (value._baseType != null) {
if (value._baseType === "guardedFunction") {
let argList = value._options.args
.map((arg) => getValueType(arg, false))
.join(", ");
return ["a", `guarded function (${argList}) → ${getValueType(value._options.returnType, false)}`];
} else {
if (value._baseType === "string") {
return ["a", "string"];
} else if (value._baseType === "number") {
return ["a", "number"];
} else if (value._baseType === "boolean") {
return ["a", "boolean"];
} else {
return [null, value._baseType];
}
}
} else if (value._modifierType != null) {
if (value._modifierType === "either") {
let typeList = value._types
.map((type) => getValueType(type, false))
.join(" | ");
return [null, `Either<${typeList}>`];
} else {
throw new Error(`Encountered unrecognized modifier type: ${value._modifierType}`);
}
} else if (value._collectionType != null) {
let collectionTypeName = capitalize(value._collectionType);
if (value._keyType != null) {
return ["a", `${collectionTypeName}<${getValueType(value._keyType, false)}${getValueType(value._itemType, false)}>`];
} else {
return ["a", `${collectionTypeName}<${getValueType(value._itemType, false)}>`];
}
} else if (value._isCustomType) {
return ["an", `instance of ${value._name}`];
} else if (value._isTrait) {
return ["an", `instance of a type with the ${value._name} trait`];
} else {
throw new Error(`Encountered unrecognized type rule: ${util.inspect(value, {breakLength: Infinity})}`);
}
} else {
/* Assume this is a value, rather than a type rule. */
if (value === undefined) {
return [null, "undefined"];
} else if (value === null) {
return [null, "null"];
} else if (typeof value === "boolean") {
return ["a", "boolean"];
} else if (typeof value === "number") {
if (value === Number.POSITIVE_INFINITY) {
return [null, "Infinity"];
} else if (value === Number.NEGATIVE_INFINITY) {
return [null, "negative Infinity"];
} else if (isNaN(value)) {
return [null, "NaN"];
} else {
return ["a", "number"];
}
} else if (typeof value === "string") {
return ["a", "string"];
} else if (typeof value === "function") {
if (value._guardedFunction != null) {
let functionName;
if (isNamedFunction(value._guardedFunction)) {
functionName = `guarded function[${value._guardedFunction.name}]`;
} else {
functionName = "guarded function";
}
/* TODO: Deduplicate this code? */
let argList = value._guardedArgs
.map((arg) => getValueType(arg, false))
.join(", ");
return ["a", `${functionName} (${argList}) → ${getValueType(value._guardedReturnType, false)}`];
} else if (isConstructor(value)) {
if (isNamedFunction(value)) {
return ["a", `constructor[${value.name}]`];
} else {
return ["a", "constructor"];
}
} else {
if (isNamedFunction(value)) {
return ["a", `function[${value.name}]`];
} else {
return ["a", "function"];
}
}
} else if (Array.isArray(value)) {
return ["an", "array"];
} else if (value instanceof RegExp) {
return ["a", "regular expression"];
} else if (typeof value === "object") {
if (value._guardedCollectionType === "set") {
return ["a", `Set<${getValueType(value._itemType, false)}>`];
} else if (value._guardedCollectionType === "map") {
return ["a", `Map<${getValueType(value._keyType, false)}${getValueType(value._itemType, false)}>`];
} else if (value instanceof Map) {
return ["a", "Map"];
} else if (value instanceof Set) {
return ["a", "Set"];
} else if (value._type != null && value._type._isCustomType != null) {
return ["an", `instance of ${value._type._name}`];
} else if (value.__proto__.constructor === Object) {
/* Object literal */
return ["an", "object"];
} else if (isNamedFunction(value.__proto__.constructor)) {
return ["an", `instance of ${value.__proto__.constructor.name}`];
} else {
return ["an", "instance of an unknown type"];
}
} else {
throw new Error("Encountered unrecognized value type");
}
}
}

@ -0,0 +1,6 @@
"use strict";
module.exports = function isConstructor(value) {
/* TODO: This isn't great. Technically a regular function could have a non-empty prototype; and a constructor could have a prototype that *looks* empty but has a parent prototype that isn't. */
return (typeof value === "function" && value.prototype != null && Object.keys(value.prototype).length > 0);
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function isNamedFunction(func) {
return (func.name != null && func.name !== "" && func.name !== "anonymous");
};

@ -0,0 +1,13 @@
"use strict";
module.exports = function nullMissingFields(data, schemaKeys) {
let nullFields = {};
schemaKeys.forEach((key) => {
if (data[key] === undefined) {
nullFields[key] = null;
}
});
return nullFields;
};

@ -0,0 +1,14 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createBooleanValidatorFunction(_options = {}) {
return function validateBoolean(value) {
if (typeof value !== "boolean") {
return new errors.ValidationError(`Expected a boolean, got ${getValueType(value)} instead`);
} else {
return true;
}
};
};

@ -0,0 +1,24 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createFunctionValidatorFunction(options = {}) {
let guardedFunctionDescription = getValueType({
_isTypeRule: true,
_baseType: "guardedFunction",
_options: options
});
// options.args
// options.returnValue
return function validateFunction(value) {
if (typeof value === "function" && value._guardedFunction != null) {
/* FIXME: Verify that the function signature matches. */
return true;
} else {
return new errors.ValidationError(`Expected ${guardedFunctionDescription}, got ${getValueType(value)} instead`);
}
};
};

@ -0,0 +1,14 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createNothingValidatorFunction(_options = {}) {
return function validateNothing(value) {
if (value != null) {
return new errors.ValidationError(`Expected nothing, got ${getValueType(value)} instead`);
} else {
return true;
}
};
};

@ -0,0 +1,14 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createNullValidatorFunction(_options = {}) {
return function validateNull(value) {
if (value !== null) {
return new errors.ValidationError(`Expected null, got ${getValueType(value)} instead`);
} else {
return true;
}
};
};

@ -0,0 +1,48 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createNumberValidatorFunction(options = {}, name = "<unknown>") {
let mayBeNaN = (options.mayBeNaN === true);
let mayBeInfinity = (options.mayBeInfinity === true);
let hasMinimum = (options.minimum != null);
let hasMaximum = (options.maximum != null);
if (hasMinimum && typeof options.minimum !== "number") {
throw new Error("Minimum value for number must be a number");
} else if (hasMaximum && typeof options.maximum !== "number") {
throw new Error("Maximum value for number must be a number");
} else if (hasMinimum && hasMaximum && options.minimum > options.maximum) {
throw new Error("Minimum value for number cannot be higher than maximum value");
} else {
return function validateNumber(value) {
/* TODO: More consistent error message format, with consistent property path metadata? */
if (typeof value !== "number") {
return new errors.ValidationError(`Expected a number, got ${getValueType(value)} instead`);
// return new errors.ValidationError(`Specified value for property '${name}' is not a number`, {
// value: value,
// property: name
// });
} else if (!mayBeNaN && isNaN(value)) {
return new errors.ValidationError(`Specified value for property '${name}' is NaN, but this is not allowed`, {
property: name
});
} else if (!mayBeInfinity && !isFinite(value)) {
return new errors.ValidationError(`Specified value for property '${name}' is an infinite value, but this is not allowed`, {
property: name
});
} else if (hasMinimum && value < options.minimum) {
return new errors.ValidationError(`Value for property '${name}' must be at least ${options.minimum}`, {
property: name
});
} else if (hasMaximum && value > options.maximum) {
return new errors.ValidationError(`Value for property '${name}' cannot be higher than ${options.maximum}`, {
property: name
});
} else {
return true;
}
};
}
};

@ -0,0 +1,28 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createStringValidatorFunction(options = {}, name = "<unknown>") {
let hasMatchCondition = (options.matches != null);
let matchCondition = options.matches;
return function validateString(value) {
if (typeof value !== "string") {
return new errors.ValidationError(`Expected a string, got ${getValueType(value)} instead`);
// return new errors.ValidationError(`Specified value for property '${name}' is not a string`, {
// value: value,
// property: name
// });
} else if (hasMatchCondition && !matchCondition.test(value)) {
/* TODO: Improve regex display */
return new errors.ValidationError(`Value for property '${name}' failed \`matches\` condition`, {
value: value,
property: name,
condition: matchCondition
});
} else {
return true;
}
};
};

@ -0,0 +1,14 @@
"use strict";
const errors = require("../errors");
const getValueType = require("../util/get-value-type");
module.exports = function createUndefinedValidatorFunction(_options = {}) {
return function validateUndefined(value) {
if (value !== undefined) {
return new errors.ValidationError(`Expected undefined, got ${getValueType(value)} instead`);
} else {
return true;
}
};
};

@ -0,0 +1,67 @@
"use strict";
const expect = require("chai").expect;
const dm = require("../src");
describe("function guards", () => {
it("should accept a correctly-behaving function", () => {
let guardedFunction = dm.guard([dm.string(), dm.number()], dm.boolean(), function (str, num) {
expect(str).to.equal("hello world");
expect(num).to.equal(42);
return true;
});
guardedFunction("hello world", 42);
});
it("should throw on an invalid return value type", () => {
let guardedFunction = dm.guard([dm.string(), dm.number()], dm.boolean(), function (str, num) {
return "not a boolean";
});
expect(() => {
guardedFunction("hello world", 42);
}).to.throw("Expected a boolean, got a string instead");
});
it("should throw on an invalid argument type", () => {
let guardedFunction = dm.guard([dm.string(), dm.number()], dm.boolean(), function (str, num) {
return true;
});
expect(() => {
guardedFunction(false, 42);
}).to.throw("Expected a string, got a boolean instead");
});
it("should throw on a missing argument", () => {
let guardedFunction = dm.guard([dm.string(), dm.number()], dm.boolean(), function (str, num) {
return true;
});
expect(() => {
guardedFunction("hello world");
}).to.throw("Value is required for property '<unknown>'");
});
it(" ... but not when that argument is optional", () => {
let guardedFunction = dm.guard([dm.string(), dm.number().optional()], dm.boolean(), function (str, num) {
expect(str).to.equal("hello world");
expect(num).to.equal(null);
return true;
});
guardedFunction("hello world");
});
it("should correctly handle defaults", () => {
let guardedFunction = dm.guard([dm.string(), dm.number().default(42)], dm.boolean(), function (str, num) {
expect(str).to.equal("hello world");
expect(num).to.equal(42);
return true;
});
guardedFunction("hello world");
});
});

@ -0,0 +1,16 @@
/*
- Types
- Simple field types
- Collection field types
- Instances of constructors
- Custom field types
- Guarded function types
- Traits
- Implementations
- Slots
- Disallow duplicate properties
- Function guards
- Debug descriptions
*/

@ -0,0 +1,437 @@
"use strict";
const expect = require("chai").expect;
const dm = require("../src");
/* FIXME: Disallow null/nothing/undefined in model definitions, as they make no semantic sense? But allow them for eg. function guards. */
/* FIXME: Registry */
/* FIXME: Test passing an already-guarded collection into a guarded collection factory - either stand-alone or in the context of a model? */
let Identity = dm.createType("Identity", {
label: dm.string(),
nickname: dm.string({
matches: /^[a-z0-9_-]+$/
}),
emailAddress: dm.string({
matches: /@/
}),
xmppAddress: dm.string({
matches: /@/
}).optional(),
notes: dm.string().default("(none)")
});
let User = dm.createType("User", {
name: dm.string(),
isActive: dm.boolean(),
registrationDate: dm.instanceOf(Date),
misc: dm.either(
dm.string(),
dm.number()
),
// undefinedValue: dm.undefined(),
// nullValue: dm.null(),
// nothingValue: dm.nothing(),
age: dm.number({
minimum: 0
}),
luckyNumbers: dm.setOf(dm.number({
minimum: 1,
maximum: 42
})),
identities: dm.setOf(Identity),
mainIdentity: Identity,
alternateUser: dm.self().optional(),
codeWords: dm.mapOf(dm.string(), dm.string())
});
function generateUserData(props = {}) {
return Object.assign({}, {
name: "Sven Slootweg",
isActive: true,
registrationDate: new Date(),
age: 26,
luckyNumbers: new Set([13, 42]),
misc: 42,
// nullValue: null,
// undefinedValue: undefined,
// nothingValue: undefined,
identities: new Set([
Identity({
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}),
Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net"
})
]),
mainIdentity: Identity({
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}),
codeWords: new Map([
["foo", "Foo"],
["bar", "Bar"]
])
}, props);
}
function compareObject(reference, obj) {
if (typeof reference === "object") {
for (let [key, value] of Object.entries(reference)) {
if (value instanceof Set) {
let objValues = Array.from(obj[key]);
let referenceValues = Array.from(value);
referenceValues.forEach((item, i) => {
compareObject(item, objValues[i]);
});
} else if (value instanceof Map) {
for (let [mapKey, item] of value.entries()) {
compareObject(item, obj[key].get(mapKey));
}
} else if (typeof reference === "object") {
compareObject(value, obj[key]);
} else {
expect(obj[key]).to.equal(value);
}
}
} else {
expect(obj).to.equal(reference);
}
}
describe("models", () => {
describe("valid cases", () => {
let userInstance;
let date = new Date();
it("should succeed at creating valid objects", () => {
userInstance = User(generateUserData({
registrationDate: date
}));
});
it("should have the correct properties", () => {
compareObject({
name: "Sven Slootweg",
isActive: true,
registrationDate: date,
age: 26,
luckyNumbers: new Set([13, 42]),
misc: 42,
identities: new Set([{
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
}, {
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net",
notes: "(none)"
}]),
mainIdentity: {
label: "primary",
nickname: "joepie91",
emailAddress: "admin@cryto.net",
xmppAddress: "joepie91@neko.im",
notes: "Main nickname"
},
codeWords: new Map([
["foo", "Foo"],
["bar", "Bar"]
])
}, userInstance);
});
it("should allow extraneous properties", () => {
let instance = User(generateUserData({
extraneousProperty: 42
}));
expect(instance.extraneousProperty).to.equal(42);
});
it("should allow a null-typed value for nothingValue as well", () => {
User(generateUserData({
nothingValue: null
}));
});
it("should allow a passing validation constraint", () => {
let Model = dm.createType("Model", {
value: dm.string().validate((value) => {
return true;
})
});
Model({
value: "foo"
});
});
// TODO: Tests for allowing NaN, Infinity, -Infinity, etc.
});
describe("failure cases", () => {
describe("required properties", () => {
it("should fail on a missing required property", () => {
expect(() => {
let data = generateUserData();
delete data.name;
User(data);
}).to.throw("Value is required for property 'name'");
});
it("should fail on a required property being null", () => {
expect(() => {
User(generateUserData({
name: null
}));
}).to.throw("Value is required for property 'name'");
});
it("should fail on a required property being undefined", () => {
expect(() => {
User(generateUserData({
name: undefined
}));
}).to.throw("Value is required for property 'name'");
});
});
describe("top-level properties", () => {
it("should require a string-typed name", () => {
expect(() => {
User(generateUserData({
name: 42
}));
}).to.throw("Expected a string, got a number instead");
});
it("should require a boolean-typed isActive", () => {
expect(() => {
User(generateUserData({
isActive: 42
}));
}).to.throw("Expected a boolean, got a number instead");
});
it("should require a number-typed age", () => {
expect(() => {
User(generateUserData({
age: "foo"
}));
}).to.throw("Expected a number, got a string instead");
});
it("should require either a number-typed or a string-typed misc value", () => {
expect(() => {
User(generateUserData({
misc: false
}));
}).to.throw("Expected one of (string, number), got a boolean instead");
});
it("should require a Date-typed registrationDate", () => {
expect(() => {
User(generateUserData({
registrationDate: 42
}));
}).to.throw("Expected an instance of Date, got a number instead");
});
it("should require a Set-typed luckyNumbers", () => {
expect(() => {
User(generateUserData({
luckyNumbers: 42
}));
}).to.throw("Expected a Set or a Set<number>, got a number instead");
});
it("should require a Map-typed codeWords", () => {
expect(() => {
User(generateUserData({
codeWords: 42
}));
}).to.throw("Expected a Map or a Map<string → string>, got a number instead");
});
});
describe("collections and nested instances", () => {
/* TODO: Do we need separate tests for guarded Maps/Sets? */
it("should require Identity-typed values for its identities Set", () => {
expect(() => {
User(generateUserData({
identities: new Set([
42, 41, 40
])
}));
}).to.throw("Expected an instance of Identity, got a number instead");
});
it("should require string-typed keys for its codeWords Map", () => {
expect(() => {
User(generateUserData({
codeWords: new Map([
[42, "foo"],
["bar", "baz"]
])
}));
}).to.throw("Expected a string, got a number instead");
});
it("should require string-typed values for its codeWords Map", () => {
expect(() => {
User(generateUserData({
codeWords: new Map([
["foo", 42],
["bar", "baz"]
])
}));
}).to.throw("Expected a string, got a number instead");
});
it("should reject an instance of the wrong custom type", () => {
expect(() => {
User(generateUserData({
mainIdentity: User(generateUserData())
}));
}).to.throw("Expected an instance of Identity, got an instance of User instead");
});
it("should reject an instance of the wrong type for a 'self' rule", () => {
expect(() => {
User(generateUserData({
alternateUser: Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "botpie91@cryto.net"
})
}));
}).to.throw("Expected an instance of User, got an instance of Identity instead");
});
it("should also validate values for a nested instance", () => {
expect(() => {
User(generateUserData({
mainIdentity: Identity({
label: "bot",
nickname: {},
emailAddress: "botpie91@cryto.net"
})
}));
}).to.throw("Expected a string, got an object instead");
});
it("should also validate values for nested instances in a Set", () => {
expect(() => {
User(generateUserData({
identities: new Set([
Identity({
label: "bot",
nickname: {},
emailAddress: "botpie91@cryto.net"
})
])
}));
}).to.throw("Expected a string, got an object instead");
});
});
describe("type options", () => {
describe("numbers", () => {
it("should detect violation of a minimum number", () => {
expect(() => {
User(generateUserData({
age: -1
}));
}).to.throw("Value for property 'age' must be at least 0");
});
it("should detect violation of a numeric range, at the lower bound", () => {
expect(() => {
User(generateUserData({
luckyNumbers: new Set([13, -1])
}));
}).to.throw("Value for property '<unknown>' must be at least 1");
});
it("should detect violation of a numeric range, at the upper bound", () => {
expect(() => {
User(generateUserData({
luckyNumbers: new Set([13, 2000])
}));
}).to.throw("Value for property '<unknown>' cannot be higher than 42");
});
it("should reject NaN", () => {
expect(() => {
User(generateUserData({
age: NaN
}));
}).to.throw("Specified value for property 'age' is NaN, but this is not allowed");
});
it("should reject positive Infinity", () => {
expect(() => {
User(generateUserData({
age: Infinity
}));
}).to.throw("Specified value for property 'age' is an infinite value, but this is not allowed");
});
it("should reject negative Infinity", () => {
expect(() => {
User(generateUserData({
age: -Infinity
}));
}).to.throw("Specified value for property 'age' is an infinite value, but this is not allowed");
});
});
describe("strings", () => {
it("should reject a string that fails the specified 'matches' regex", () => {
expect(() => {
User(generateUserData({
mainIdentity: Identity({
label: "bot",
nickname: "botpie91",
emailAddress: "not-an-email-address"
})
}));
}).to.throw("Value for property 'emailAddress' failed `matches` condition");
});
});
/* TODO: Function guards */
});
describe("additional constraints", () => {
it("should reject a failing validation constraint", () => {
let Model = dm.createType("Model", {
value: dm.string().validate((value) => {
return new dm.ValidationError("Value failed nonsense validation rule");
})
});
expect(() => {
Model({
value: "foo"
});
}).to.throw("Value failed nonsense validation rule");
});
});
});
});

@ -0,0 +1,143 @@
"use strict";
const expect = require("chai").expect;
const dm = require("../src");
/* FIXME: Slot rule testing */
/* FIXME: Function guard signature matching */
let GitSource = dm.createTrait("GitSource", {
stringify: dm.function([], dm.string())
});
let GithubSource, CrytoGitSource, ExternalGitSource, sourceOne, sourceTwo, randomGitSource;
describe("traits", () => {
it("should allow valid trait implementations", () => {
GithubSource = dm.createType("GithubSource", {
username: dm.string(),
repository: dm.string()
}).implements(GitSource, {
stringify: dm.guard([], dm.string(), function () {
return `https://github.com/${this.username}/${this.repository}.git`;
})
});
CrytoGitSource = dm.createType("CrytoGitSource", {
username: dm.string(),
repository: dm.string()
}).implements(GitSource, {
stringify: dm.guard([], dm.string(), function () {
return `http://git.cryto.net/${this.username}/${this.repository}.git`;
})
});
});
it("should produce working trait functionality", () => {
sourceOne = GithubSource({
username: "joepie91",
repository: "node-bhttp"
});
expect(sourceOne.stringify()).to.equal("https://github.com/joepie91/node-bhttp.git");
sourceTwo = CrytoGitSource({
username: "joepie91",
repository: "node-bhttp"
});
expect(sourceTwo.stringify()).to.equal("http://git.cryto.net/joepie91/node-bhttp.git");
});
let guardedFunc = dm.guard([GitSource], dm.nothing(), function (source) {
expect(source.stringify()).to.be.a("string");
});
it("should allow an instance of any type with the correct trait to be passed into a trait-guarded function", () => {
guardedFunc(sourceOne);
guardedFunc(sourceTwo);
});
it("should reject an instance of a type that does not have the correct trait", () => {
let SomeType = dm.createType("SomeType", {
value: dm.string()
});
let instance = SomeType({
value: "foo"
});
expect(() => {
guardedFunc(instance);
}).to.throw("Expected object of a type with the GitSource trait, got an instance of SomeType instead");
});
it(" ... even if that type has an otherwise compatible-looking method", () => {
let SomeOtherType = dm.createType("SomeOtherType", {
value: dm.string(),
stringify: dm.function([], dm.string())
});
let instance = SomeOtherType({
value: "foo",
stringify: dm.guard([], dm.string(), function () {
return "this is not a URL at all";
})
});
expect(() => {
guardedFunc(instance);
}).to.throw("Expected object of a type with the GitSource trait, got an instance of SomeOtherType instead");
});
it("should reject a trait implementation that does not meet the trait definition", () => {
expect(() => {
let InvalidType = dm.createType("InvalidType", {
value: dm.string()
}).implements(GitSource, {
stringify: "this is not a function"
});
}).to.throw("Expected a guarded function () → string, got a string instead");
});
it("should reject an empty trait implementation when the trait definition does not allow that", () => {
expect(() => {
let InvalidType = dm.createType("InvalidType", {
value: dm.string()
}).implements(GitSource, {
});
}).to.throw("Value is required for property 'stringify'");
});
it("should allow for specifying slot rules", () => {
ExternalGitSource = dm.createType("ExternalGitSource", {
sourceName: dm.string()
}).implements(GitSource, {
stringify: dm.slot()
});
});
it("should allow specifying a value for that slot", () => {
randomGitSource = ExternalGitSource({
sourceName: "randomGit",
stringify: dm.guard([], dm.string(), function () {
return "https://randomgit.example.com/repo.git"
})
});
});
it("should have produced a working slot value", () => {
expect(randomGitSource.stringify()).to.equal("https://randomgit.example.com/repo.git");
});
it("should reject an invalid slot value in the instance", () => {
expect(() => {
ExternalGitSource({
sourceName: "randomGit",
stringify: "foo"
});
}).to.throw("Expected a guarded function () → string, got a string instead");
});
});

@ -0,0 +1,275 @@
"use strict";
const expect = require("chai").expect;
const dm = require("../src");
const getValueType = require("../src/util/get-value-type");
const generateValidator = require("../src/generate-validator");
const createGuardedMap = require("../src/guarded-collections/map");
const createGuardedSet = require("../src/guarded-collections/set");
describe("value descriptions", () => {
describe("values", () => {
describe("simple types", () => {
it("should correctly describe a string", () => {
expect(getValueType("some string")).to.equal("a string");
});
it("should correctly describe a number", () => {
expect(getValueType(42)).to.equal("a number");
});
it("should correctly describe a boolean", () => {
expect(getValueType(true)).to.equal("a boolean");
});
it("should correctly describe null", () => {
expect(getValueType(null)).to.equal("null");
});
it("should correctly describe undefined", () => {
expect(getValueType(undefined)).to.equal("undefined");
});
it("should correctly describe positive Infinity", () => {
expect(getValueType(Infinity)).to.equal("Infinity");
});
it("should correctly describe negative Infinity", () => {
expect(getValueType(-Infinity)).to.equal("negative Infinity");
});
it("should correctly describe NaN", () => {
expect(getValueType(NaN)).to.equal("NaN");
});
it("should correctly describe a RegExp", () => {
expect(getValueType(new RegExp())).to.equal("a regular expression");
});
it("should correctly describe a regular expression literal", () => {
expect(getValueType(/foo/)).to.equal("a regular expression");
});
});
describe("function types", () => {
it("should correctly describe a named function", () => {
function namedFunction() {
// nothing
}
expect(getValueType(namedFunction)).to.equal("a function[namedFunction]");
});
it("should correctly describe an anonymous function", () => {
expect(getValueType(function(){})).to.equal("a function");
});
it("should correctly describe an arrow function", () => {
expect(getValueType(() => {})).to.equal("a function");
});
it("should correctly describe a function created through a Function object", () => {
expect(getValueType(new Function())).to.equal("a function");
});
it("should correctly describe a named constructor function", () => {
function SomeConstructor() {
// nothing
}
SomeConstructor.prototype.someProp = 42;
expect(getValueType(SomeConstructor)).to.equal("a constructor[SomeConstructor]");
});
it("should correctly describe an anonymous constructor function", () => {
/* NOTE: We're doing some weird IIFE wrapping here to defeat the function name inference introduced in ES6; otherwise we can't test anonymous constructors. */
(function (SomeConstructor) {
SomeConstructor.prototype.someProp = 42;
expect(getValueType(SomeConstructor)).to.equal("a constructor");
})(function(){});
});
it("should correctly describe a guarded named function", () => {
let func = dm.guard(
[dm.string(), dm.boolean()],
dm.nothing(),
function someNamedFunction(stringArg, booleanArg) {
// nothing
}
);
expect(getValueType(func)).to.equal("a guarded function[someNamedFunction] (string, boolean) → nothing")
});
it("should correctly describe a guarded anonymous function", () => {
let func = dm.guard(
[dm.string(), dm.boolean()],
dm.nothing(),
function (stringArg, booleanArg) {
// nothing
}
);
expect(getValueType(func)).to.equal("a guarded function (string, boolean) → nothing")
});
});
describe("collection types", () => {
it("should correctly describe an array", () => {
expect(getValueType([])).to.equal("an array");
});
it("should correctly describe an object literal", () => {
expect(getValueType({})).to.equal("an object");
});
it("should correctly describe a Map", () => {
expect(getValueType(new Map())).to.equal("a Map");
});
it("should correctly describe a Set", () => {
expect(getValueType(new Set())).to.equal("a Set");
});
/* TODO: Guarded array */
it("should correctly describe a guarded Map", () => {
let guardedMap = createGuardedMap(new Map(), dm.boolean(), dm.string(), {});
expect(getValueType(guardedMap)).to.equal("a Map<string → boolean>");
});
it("should correctly describe a guarded Set", () => {
let guardedSet = createGuardedSet(new Set(), dm.boolean(), null, {});
expect(getValueType(guardedSet)).to.equal("a Set<boolean>");
});
});
describe("instance types", () => {
describe("standard types", () => {
it("should correctly describe a Date", () => {
expect(getValueType(new Date())).to.equal("an instance of Date");
});
it("should correctly describe an Error", () => {
expect(getValueType(new Error())).to.equal("an instance of Error");
});
});
describe("custom types", () => {
it("should correctly describe a type", () => {
let CustomType = dm.createType("CustomType", {
value: dm.string()
});
expect(getValueType(CustomType({
value: "foo"
}))).to.equal("an instance of CustomType");
});
});
});
});
describe("rules", () => {
describe("simple types", () => {
it("should correctly describe a boolean rule", () => {
expect(getValueType(dm.boolean())).to.equal("a boolean");
});
it("should correctly describe a string rule", () => {
expect(getValueType(dm.string())).to.equal("a string");
});
it("should correctly describe a number rule", () => {
expect(getValueType(dm.number())).to.equal("a number");
});
it("should correctly describe a null rule", () => {
expect(getValueType(dm.null())).to.equal("null");
});
it("should correctly describe a undefined rule", () => {
expect(getValueType(dm.undefined())).to.equal("undefined");
});
it("should correctly describe a nothing rule", () => {
expect(getValueType(dm.nothing())).to.equal("nothing");
});
it("should correctly describe an instanceOf rule", () => {
expect(getValueType(dm.instanceOf(Date))).to.equal("an instance of Date");
});
});
describe("collection types", () => {
it("should correctly describe a setOf rule", () => {
let rule = dm.setOf(dm.boolean());
expect(getValueType(rule)).to.equal("a Set<boolean>");
});
it("should correctly describe a mapOf rule", () => {
let rule = dm.mapOf(dm.string(), dm.boolean());
expect(getValueType(rule)).to.equal("a Map<string → boolean>");
});
/* TODO: Guarded arrays */
});
describe("custom types", () => {
it("should correctly describe a type", () => {
let CustomType = dm.createType("CustomType", {
value: dm.string()
});
expect(getValueType(CustomType)).to.equal("an instance of CustomType");
});
it("should correctly describe a trait", () => {
let CustomTrait = dm.createTrait("CustomTrait", {
value: dm.string()
});
expect(getValueType(CustomTrait)).to.equal("an instance of a type with the CustomTrait trait");
});
});
describe("modifiers", () => {
it("should correctly describe an either rule", () => {
let rule = dm.either(
dm.string(),
dm.boolean()
);
expect(getValueType(rule)).to.equal("Either<string | boolean>");
});
});
describe("special rules", () => {
it("should correctly describe a self rule", () => {
expect(getValueType(dm.self())).to.equal("(self)");
});
it("should correctly describe a slot rule", () => {
expect(getValueType(dm.slot())).to.equal("(slot)");
});
});
describe("guards", () => {
it("should correctly describe a guarded function rule", () => {
expect(getValueType(dm.function(
[dm.string(), dm.boolean()],
dm.nothing(),
))).to.equal("a guarded function (string, boolean) → nothing");
});
});
describe("error conditions", () => {
it("should throw an error when encountering an unrecognized rule", () => {
expect(() => {
getValueType({_isTypeRule: true, _butNotReally: false});
}).to.throw();
});
});
});
});
Loading…
Cancel
Save