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