Add type registry support

master
Sven Slootweg 6 years ago
parent e16224604a
commit 7118d696bb

@ -10,15 +10,16 @@ const nullMissingFields = require("./util/null-missing-fields");
/* TODO: Traits implementing traits */
module.exports = function createTrait(name, schema) {
module.exports = function createTrait(name, schema, options = {}) {
let schemaDescriptors = mapObj(schema, (key, rule) => {
return generateDescriptor(rule, key, true);
return generateDescriptor(rule, key, true, options._registry);
});
let schemaKeys = getSchemaKeys(schema);
return typeRules._createTypeRule({
_isTrait: true,
_registry: options._registry,
_name: name,
applyImplementation: function (implementation) {
/* FIXME: Verify that there are no extraneous keys or unspecified values */
@ -43,9 +44,9 @@ module.exports = function createTrait(name, schema) {
let implementationDescriptors = mapObj(implementation, (key, value) => {
if (value != null && value._isSlotRule === true) {
return generateDescriptor(schema[key], key, true);
return generateDescriptor(schema[key], key, true, options._registry);
} else {
return generateDescriptor(value, key);
return generateDescriptor(value, key, false, options._registry);
}
});

@ -8,16 +8,17 @@ 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) {
module.exports = function createType(name, schema, options = {}) {
if (schema._isTypeRule === true) {
return typeRules._createTypeRule({
_typeName: name,
_isTypeAlias: true,
_alias: schema
_alias: schema,
_registry: options._registry
});
} else {
let propertyDescriptors = mapObj(schema, (key, rule) => {
return generateDescriptor(rule, key);
return generateDescriptor(rule, key, false, options._registry);
});
let protoProperties = {
@ -54,18 +55,18 @@ module.exports = function createType(name, schema) {
proto._type = factory;
factory._registry = options._registry;
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);
};
factory._validator = function (value) {
return (value instanceof factory);
};
proto._selfValidator = factory._createValidator();
/* FIXME: Is the below actually necessary? */
proto._selfValidator = factory._validator;
factory._isUsed = false;
factory._implementedTraits = new Set();

@ -4,9 +4,9 @@ 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) {
module.exports = function generateDescriptor(rule, key, allowSlot = false, registry) {
if (rule._isTypeRule === true) {
let validator = generateValidator(rule, key);
let validator = generateValidator(rule, key, registry);
if (rule._collectionType != null) {
let guardedCollectionFactory;
@ -19,8 +19,8 @@ module.exports = function generateDescriptor(rule, key, allowSlot = false) {
throw new Error(`Unknown collection type: ${rule._collectionType}`);
}
let valueGuard = generateValidator(rule._itemType);
let keyGuard = (rule._keyType != null) ? generateValidator(rule._keyType) : null;
let valueGuard = generateValidator(rule._itemType, undefined, registry);
let keyGuard = (rule._keyType != null) ? generateValidator(rule._keyType, undefined, registry) : null;
return [key, {
enumerable: true,

@ -12,7 +12,7 @@ const createUndefinedValidator = require("./validator-functions/undefined");
const getValueType = require("./util/get-value-type");
const errors = require("./errors");
module.exports = function generateValidator(rule, name = "<unknown>") {
module.exports = function generateValidator(rule, name = "<unknown>", registry) {
let baseRule;
if (rule._baseType != null) {
@ -37,7 +37,7 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
baseRule = () => true; /* FIXME */
} else if (rule._modifierType != null) {
if (rule._modifierType === "either") {
let validators = rule._types.map((type) => generateValidator(type));
let validators = rule._types.map((type) => generateValidator(type, undefined, registry));
baseRule = function (value) {
let matched = false;
@ -60,10 +60,10 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
throw new Error(`Unrecognized modifier: ${rule._modifierType}`);
}
} else if (rule._isTypeAlias === true) {
baseRule = generateValidator(rule._alias, name);
baseRule = generateValidator(rule._alias, name, registry);
} else if (rule._isCustomType === true) {
let validator = rule._validator;
let validator = rule._createValidator();
baseRule = function (value) {
if (validator.call(this, value) === true) {
return true;
@ -71,6 +71,16 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`);
}
};
} else if (rule._isCustomRegistryType) {
baseRule = function (value) {
/* FIXME: Better error when the type is unknown */
if (registry._types.get(rule._name)._validator.call(this, value) === true) {
return true;
} else {
/* FIXME: Add support for registry rules to getValueType */
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)) {
@ -79,6 +89,22 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
return new errors.ValidationError(`Expected object of a type with the ${rule._name} trait, got ${getValueType(value)} instead`);
}
};
} else if (rule._isRegistryTrait === true) {
if (registry == null) {
throw new Error("Registry-based type rules can only be used within the context of a type registry");
} else {
baseRule = function (value) {
/* TODO: The below approach requires that traits be defined before the types that use them, and disallows trait registry references in `.implements` calls, due to _implementedTraits always needing to contain actual trait definitions; in the future, a better approach needs to be found for this such that trait registry references can be used everywhere. */
let actualRule = registry._traits.get(rule._name);
/* FIXME: Better error when the trait is unknown */
if (value._type != null && value._type._implementedTraits != null && value._type._implementedTraits.has(actualRule)) {
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) {
@ -150,7 +176,7 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
}
} 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) {
for (let rule of rules) {
let result = rule.call(this, value);
if (result === true) {

@ -2,12 +2,12 @@
const generateValidator = require("./generate-validator");
module.exports = function guardFunction(args, returnType, func) {
module.exports = function guardFunction(args, returnType, func, _registry) {
let rules = args.map((arg) => {
return generateValidator(arg);
return generateValidator(arg, undefined, _registry);
});
let returnValueValidator = generateValidator(returnType);
let returnValueValidator = generateValidator(returnType, undefined, _registry);
let guardedFunction = function (...params) {
let paramsWithDefaults = new Array(params.length);

@ -1,10 +1,7 @@
"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");
const moduleAPI = require("./module-api");
const createRegistry = require("./registry");
/* 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
@ -24,8 +21,5 @@ TODO: Add full property paths to errors?
*/
module.exports = Object.assign({
createType: createType,
createTrait: createTrait,
guard: guardFunction,
ValidationError: errors.ValidationError
}, typeRules);
createRegistry: createRegistry
}, moduleAPI);

@ -0,0 +1,14 @@
"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");
module.exports = Object.assign({
createType: createType,
createTrait: createTrait,
guard: guardFunction,
ValidationError: errors.ValidationError
}, typeRules);

@ -0,0 +1,47 @@
"use strict";
const moduleAPI = require("./module-api");
const typeRules = require("./type-rules");
/* FIXME: Disallow usage of registry.type-style references from one registry in types of another? Or perhaps embed the registry in the reference, instead of passing it through functions by means of scope? */
module.exports = function createRegistry() {
return Object.assign({}, moduleAPI, {
_types: new Map(),
_traits: new Map(),
createType: function (name, schema, options = {}) {
let combinedOptions = Object.assign({
_registry: this
}, options);
let type = moduleAPI.createType(name, schema, combinedOptions);
this._types.set(name, type);
return type;
},
createTrait: function (name, schema, options = {}) {
let combinedOptions = Object.assign({
_registry: this
}, options);
let trait = moduleAPI.createTrait(name, schema, combinedOptions);
this._traits.set(name, trait);
return trait;
},
guardFunction: function (args, returnType, func) {
return moduleAPI.guardFunction(args, returnType, func, this);
},
type: function (typeName) {
return typeRules._createTypeRule({
_isCustomRegistryType: true,
_name: typeName
});
},
trait: function (traitName) {
return typeRules._createTypeRule({
_isRegistryTrait: true,
_name: traitName
});
},
createRegistry: undefined
});
};

@ -97,9 +97,9 @@ function getValueTypeData(value) {
} else {
return ["a", `${collectionTypeName}<${getValueType(value._itemType, false)}>`];
}
} else if (value._isCustomType) {
} else if (value._isCustomType || value._isCustomRegistryType) {
return ["an", `instance of ${value._name}`];
} else if (value._isTrait) {
} else if (value._isTrait || value._isRegistryTrait) {
return ["an", `instance of a type with the ${value._name} trait`];
} else {
throw new Error(`Encountered unrecognized type rule: ${util.inspect(value, {breakLength: Infinity})}`);

@ -0,0 +1,127 @@
"use strict";
const expect = require("chai").expect;
const dm = require("../src");
describe("registry", () => {
let registry = dm.createRegistry();
describe("registry API", () => {
it("should not expose registry-specific methods on the primary module API", () => {
expect(dm.type).to.equal(undefined);
expect(dm.trait).to.equal(undefined);
});
it("should expose the createRegistry method on the primary module API", () => {
expect(dm.createRegistry).to.be.a("function");
});
it("should expose registry-specific methods on the registry API", () => {
expect(registry.type).to.be.a("function");
expect(registry.trait).to.be.a("function");
});
it("should not expose the createRegistry method on the registry API", () => {
expect(registry.createRegistry).to.equal(undefined);
});
});
describe("registry usage", () => {
it("should work correctly for types", () => {
let Person = registry.createType("Person", {
name: dm.string(),
favouriteGift: registry.type("Gift").optional()
});
let Gift = registry.createType("Gift", {
description: dm.string(),
from: registry.type("Person"),
to: registry.type("Person")
});
let joe = Person({
name: "Joe"
});
let jane = Person({
name: "Jane"
});
let flowers = Gift({
description: "Flowers",
from: jane,
to: joe
});
joe.favouriteGift = flowers;
expect(joe.favouriteGift).to.equal(flowers);
expect(flowers.to).to.equal(joe);
expect(flowers.from).to.equal(jane);
expect(() => {
jane.favouriteGift = "not a gift";
}).to.throw("Expected an instance of Gift, got a string instead");
expect(() => {
flowers.to = "not a person";
}).to.throw("Expected an instance of Person, got a string instead");
expect(() => {
flowers.to = flowers;
}).to.throw("Expected an instance of Person, got an instance of Gift instead");
});
it("should work correctly for traits", () => {
let Givable = registry.createTrait("Givable", {
from: registry.type("Person"),
to: registry.type("Person")
});
let Person = registry.createType("Person", {
name: dm.string(),
favouriteGift: registry.trait("Givable").optional()
});
let Gift = registry.createType("Gift", {
description: dm.string()
}).implements(Givable, {
from: dm.slot(),
to: dm.slot()
});
let joe = Person({
name: "Joe"
});
let jane = Person({
name: "Jane"
});
let flowers = Gift({
description: "Flowers",
from: jane,
to: joe
});
joe.favouriteGift = flowers;
expect(joe.favouriteGift).to.equal(flowers);
expect(flowers.to).to.equal(joe);
expect(flowers.from).to.equal(jane);
expect(() => {
jane.favouriteGift = "not a gift";
}).to.throw("Expected object of a type with the Givable trait, got a string instead");
expect(() => {
flowers.to = "not a person";
}).to.throw("Expected an instance of Person, got a string instead");
expect(() => {
flowers.to = flowers;
}).to.throw("Expected an instance of Person, got an instance of Gift instead");
});
});
});
Loading…
Cancel
Save