From 7118d696bbc5648e58d2982cdca4ffd624e57a47 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Wed, 8 Aug 2018 09:22:40 +0200 Subject: [PATCH] Add type registry support --- src/create-trait.js | 9 +-- src/create-type.js | 17 ++--- src/generate-descriptor.js | 8 +-- src/generate-validator.js | 36 +++++++++-- src/guard-function.js | 6 +- src/index.js | 14 ++-- src/module-api.js | 14 ++++ src/registry.js | 47 ++++++++++++++ src/util/get-value-type.js | 4 +- test/registry.js | 127 +++++++++++++++++++++++++++++++++++++ 10 files changed, 246 insertions(+), 36 deletions(-) create mode 100644 src/module-api.js create mode 100644 src/registry.js create mode 100644 test/registry.js diff --git a/src/create-trait.js b/src/create-trait.js index a9327db..49f952a 100644 --- a/src/create-trait.js +++ b/src/create-trait.js @@ -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); } }); diff --git a/src/create-type.js b/src/create-type.js index e7a88e0..cfeb898 100644 --- a/src/create-type.js +++ b/src/create-type.js @@ -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(); diff --git a/src/generate-descriptor.js b/src/generate-descriptor.js index d70b908..25cdc57 100644 --- a/src/generate-descriptor.js +++ b/src/generate-descriptor.js @@ -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, diff --git a/src/generate-validator.js b/src/generate-validator.js index 05d1e48..a5f335a 100644 --- a/src/generate-validator.js +++ b/src/generate-validator.js @@ -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 = "") { +module.exports = function generateValidator(rule, name = "", registry) { let baseRule; if (rule._baseType != null) { @@ -37,7 +37,7 @@ module.exports = function generateValidator(rule, name = "") { 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 = "") { 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 = "") { 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 = "") { 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 = "") { } } 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) { diff --git a/src/guard-function.js b/src/guard-function.js index 9580db5..3a8fb09 100644 --- a/src/guard-function.js +++ b/src/guard-function.js @@ -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); diff --git a/src/index.js b/src/index.js index 8d13a0c..42f3bb9 100644 --- a/src/index.js +++ b/src/index.js @@ -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); diff --git a/src/module-api.js b/src/module-api.js new file mode 100644 index 0000000..4309611 --- /dev/null +++ b/src/module-api.js @@ -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); diff --git a/src/registry.js b/src/registry.js new file mode 100644 index 0000000..ed28ded --- /dev/null +++ b/src/registry.js @@ -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 + }); +}; diff --git a/src/util/get-value-type.js b/src/util/get-value-type.js index 764d441..b666fd7 100644 --- a/src/util/get-value-type.js +++ b/src/util/get-value-type.js @@ -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})}`); diff --git a/test/registry.js b/test/registry.js new file mode 100644 index 0000000..05dfdbe --- /dev/null +++ b/test/registry.js @@ -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"); + }); + }); +});