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 */ /* TODO: Traits implementing traits */
module.exports = function createTrait(name, schema) { module.exports = function createTrait(name, schema, options = {}) {
let schemaDescriptors = mapObj(schema, (key, rule) => { let schemaDescriptors = mapObj(schema, (key, rule) => {
return generateDescriptor(rule, key, true); return generateDescriptor(rule, key, true, options._registry);
}); });
let schemaKeys = getSchemaKeys(schema); let schemaKeys = getSchemaKeys(schema);
return typeRules._createTypeRule({ return typeRules._createTypeRule({
_isTrait: true, _isTrait: true,
_registry: options._registry,
_name: name, _name: name,
applyImplementation: function (implementation) { applyImplementation: function (implementation) {
/* FIXME: Verify that there are no extraneous keys or unspecified values */ /* 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) => { let implementationDescriptors = mapObj(implementation, (key, value) => {
if (value != null && value._isSlotRule === true) { if (value != null && value._isSlotRule === true) {
return generateDescriptor(schema[key], key, true); return generateDescriptor(schema[key], key, true, options._registry);
} else { } 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 getSchemaKeys = require("./util/get-schema-keys");
const nullMissingFields = require("./util/null-missing-fields"); const nullMissingFields = require("./util/null-missing-fields");
module.exports = function createType(name, schema) { module.exports = function createType(name, schema, options = {}) {
if (schema._isTypeRule === true) { if (schema._isTypeRule === true) {
return typeRules._createTypeRule({ return typeRules._createTypeRule({
_typeName: name, _typeName: name,
_isTypeAlias: true, _isTypeAlias: true,
_alias: schema _alias: schema,
_registry: options._registry
}); });
} else { } else {
let propertyDescriptors = mapObj(schema, (key, rule) => { let propertyDescriptors = mapObj(schema, (key, rule) => {
return generateDescriptor(rule, key); return generateDescriptor(rule, key, false, options._registry);
}); });
let protoProperties = { let protoProperties = {
@ -54,18 +55,18 @@ module.exports = function createType(name, schema) {
proto._type = factory; proto._type = factory;
factory._registry = options._registry;
factory._schemaKeys = schemaKeys; factory._schemaKeys = schemaKeys;
factory._isCustomType = true; factory._isCustomType = true;
factory._name = name; factory._name = name;
factory.prototype = proto; factory.prototype = proto;
factory.description = name; factory.description = name;
factory._createValidator = function () { factory._validator = function (value) {
return function (value) { return (value instanceof factory);
return (value instanceof factory);
};
}; };
proto._selfValidator = factory._createValidator(); /* FIXME: Is the below actually necessary? */
proto._selfValidator = factory._validator;
factory._isUsed = false; factory._isUsed = false;
factory._implementedTraits = new Set(); factory._implementedTraits = new Set();

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

@ -12,7 +12,7 @@ const createUndefinedValidator = require("./validator-functions/undefined");
const getValueType = require("./util/get-value-type"); const getValueType = require("./util/get-value-type");
const errors = require("./errors"); const errors = require("./errors");
module.exports = function generateValidator(rule, name = "<unknown>") { module.exports = function generateValidator(rule, name = "<unknown>", registry) {
let baseRule; let baseRule;
if (rule._baseType != null) { if (rule._baseType != null) {
@ -37,7 +37,7 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
baseRule = () => true; /* FIXME */ baseRule = () => true; /* FIXME */
} else if (rule._modifierType != null) { } else if (rule._modifierType != null) {
if (rule._modifierType === "either") { 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) { baseRule = function (value) {
let matched = false; let matched = false;
@ -60,10 +60,10 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
throw new Error(`Unrecognized modifier: ${rule._modifierType}`); throw new Error(`Unrecognized modifier: ${rule._modifierType}`);
} }
} else if (rule._isTypeAlias === true) { } else if (rule._isTypeAlias === true) {
baseRule = generateValidator(rule._alias, name); baseRule = generateValidator(rule._alias, name, registry);
} else if (rule._isCustomType === true) { } else if (rule._isCustomType === true) {
let validator = rule._validator;
let validator = rule._createValidator();
baseRule = function (value) { baseRule = function (value) {
if (validator.call(this, value) === true) { if (validator.call(this, value) === true) {
return 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`); 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) { } else if (rule._isTrait === true) {
baseRule = function (value) { baseRule = function (value) {
if (value._type != null && value._type._implementedTraits != null && value._type._implementedTraits.has(rule)) { 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`); 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) { } else if (rule._isSelfRule === true) {
baseRule = function (value) { baseRule = function (value) {
if (value instanceof this._type) { if (value instanceof this._type) {
@ -150,7 +176,7 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
} }
} else { } 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. */ /* 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); let result = rule.call(this, value);
if (result === true) { if (result === true) {

@ -2,12 +2,12 @@
const generateValidator = require("./generate-validator"); 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) => { 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 guardedFunction = function (...params) {
let paramsWithDefaults = new Array(params.length); let paramsWithDefaults = new Array(params.length);

@ -1,10 +1,7 @@
"use strict"; "use strict";
const typeRules = require("./type-rules"); const moduleAPI = require("./module-api");
const createType = require("./create-type"); const createRegistry = require("./registry");
const createTrait = require("./create-trait");
const guardFunction = require("./guard-function");
const errors = require("./errors");
/* Traits: /* 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 - 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({ module.exports = Object.assign({
createType: createType, createRegistry: createRegistry
createTrait: createTrait, }, moduleAPI);
guard: guardFunction,
ValidationError: errors.ValidationError
}, typeRules);

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