Initial commit
commit
98fbf1d556
@ -0,0 +1 @@
|
|||||||
|
test
|
@ -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" ],
|
||||||
|
}
|
||||||
|
};
|
@ -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);
|
@ -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…
Reference in New Issue