Initial serialization implementation

master
Sven Slootweg 6 years ago
parent b630797895
commit d0079c6bac

60
package-lock.json generated

@ -22,8 +22,8 @@
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"dev": true,
"requires": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
"lodash": "4.17.10",
"platform": "1.3.5"
}
},
"brace-expansion": {
@ -32,7 +32,7 @@
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
@ -53,12 +53,12 @@
"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"
"assertion-error": "1.1.0",
"check-error": "1.0.2",
"deep-eql": "3.0.1",
"get-func-name": "2.0.0",
"pathval": "1.1.0",
"type-detect": "4.0.8"
}
},
"check-error": {
@ -99,7 +99,15 @@
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
"type-detect": "4.0.8"
}
},
"default-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/default-value/-/default-value-1.0.0.tgz",
"integrity": "sha1-jG9SpaEZP+eP3J+G63HRbJdXyDo=",
"requires": {
"es6-promise-try": "0.0.1"
}
},
"diff": {
@ -108,6 +116,11 @@
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
"dev": true
},
"es6-promise-try": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/es6-promise-try/-/es6-promise-try-0.0.1.tgz",
"integrity": "sha1-EPFA2tJ0Wc75SZc+XSGgh/cnSyA="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -137,12 +150,12 @@
"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"
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"growl": {
@ -169,8 +182,8 @@
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
@ -196,7 +209,7 @@
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
"brace-expansion": "1.1.11"
}
},
"minimist": {
@ -239,6 +252,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"nanoid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.2.0.tgz",
"integrity": "sha512-rJvd0q5Bq375+jrMAJh8vZk+0Q4lnHyuqZL2fbrc9moYy4DCld5VSycYLXvwFHbbut1+UcjA+fm0bq4ADVBYQg=="
},
"object-hash": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.0.tgz",
@ -250,7 +268,7 @@
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
"wrappy": "1.0.2"
}
},
"path-is-absolute": {
@ -277,7 +295,7 @@
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
"has-flag": "3.0.0"
}
},
"type-detect": {

@ -28,8 +28,10 @@
"dependencies": {
"capitalize": "^1.0.0",
"create-error": "^0.3.1",
"default-value": "^1.0.0",
"filter-obj": "^1.1.0",
"map-obj": "^2.0.0",
"nanoid": "^1.2.0",
"object-hash": "^1.3.0"
}
}

@ -20,6 +20,7 @@ module.exports = function createTrait(name, schema, options = {}) {
return typeRules._createTypeRule({
_isTrait: true,
_name: name,
_schema: schema,
applyImplementation: function (implementation) {
/* FIXME: Verify that there are no extraneous keys or unspecified values */
let validatedImplementation = Object.create({
@ -33,29 +34,29 @@ module.exports = function createTrait(name, schema, options = {}) {
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) => {
let implementationSchema = mapObj(implementation, (key, value) => {
if (value != null && value._isSlotRule === true) {
return generateDescriptor(schema[key], key, true);
return [key, {
isSlot: true,
rule: schema[key]
}];
} else {
return generateDescriptor(value, key, false);
return [key, {
isSlot: false,
rule: value
}];
}
});
/* FIXME: Get rid of duplication here somehow? */
let slotSchemaKeys = Object.keys(implementation).filter((key) => {
let value = implementation[key];
return (value != null && value._isSlotRule != null);
let implementationDescriptors = mapObj(implementationSchema, (key, value) => {
return generateDescriptor(value.rule, key, value.isSlot);
});
return {implementationDescriptors, slotSchemaKeys};
let slotSchemaKeys = Object.keys(filterObj(implementationSchema, (key, value) => {
return value.isSlot;
}));
return {implementationSchema, implementationDescriptors, slotSchemaKeys};
}
});
};

@ -1,12 +1,28 @@
"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");
const generateTypeHash = require("./type-hash");
const serialize = require("./serialization/serialize");
const deserialize = require("./serialization/deserialize");
let registeredTypes = new Set();
let registeredTypesDirty = false;
let typeMap = new Map();
function hashAllTypes() {
if (registeredTypesDirty) {
registeredTypesDirty = false;
for (let type of registeredTypes) {
type._ensureHash();
}
}
}
module.exports = function createType(name, schema) {
if (schema._isTypeRule === true) {
@ -25,7 +41,13 @@ module.exports = function createType(name, schema) {
/* 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];
})
}),
/* FIXME: Disallow overriding this. */
serialize: function (serializer) {
return serialize(this, {
serializer: serializer
});
}
};
let proto = Object.create(protoProperties, propertyDescriptors);
@ -55,24 +77,40 @@ module.exports = function createType(name, schema) {
proto._type = factory;
factory._schemaKeys = schemaKeys;
factory._cumulativeSchema = Object.assign({}, schema);
factory._isUsed = false;
factory._typeHash = null;
factory._isCustomType = true;
factory._name = name;
factory.prototype = proto;
factory.description = name;
factory._validator = function (value) {
return (value instanceof factory);
};
factory._ensureHash = function () {
if (this._typeHash == null) {
this._isUsed = true;
this._typeHash = generateTypeHash(this);
typeMap.set(this._typeHash, this);
}
};
/* FIXME: Is the below actually necessary? */
proto._selfValidator = factory._validator;
factory._isUsed = false;
factory._implementedTraits = new Set();
factory.implements = function (trait, implementation) {
if (factory._isUsed !== false) {
/* FIXME: Deregister old hash from global hash registry after mutating through trait implementation */
if (this._isUsed !== false) {
throw new Error("Cannot modify a custom type that has already been instantiated");
} else {
let {implementationDescriptors, slotSchemaKeys} = trait.applyImplementation(implementation);
let {implementationDescriptors, slotSchemaKeys, implementationSchema} = trait.applyImplementation(implementation);
let plainImplementationSchema = mapObj(implementationSchema, (key, value) => {
return [key, value.rule];
});
// FIXME: Test this
Object.keys(implementationDescriptors).forEach((key) => {
@ -82,13 +120,30 @@ module.exports = function createType(name, schema) {
});
Object.defineProperties(proto, implementationDescriptors);
factory._implementedTraits.add(trait);
factory._schemaKeys = factory._schemaKeys.concat(slotSchemaKeys);
this._implementedTraits.add(trait);
this._schemaKeys = this._schemaKeys.concat(slotSchemaKeys);
Object.assign(this._cumulativeSchema, plainImplementationSchema);
/* TODO: Verify that this produces the expected performance benefit for trait-having typesw. */
for (let key of slotSchemaKeys) {
protoProperties._data[key] = null;
}
return this;
}
};
return typeRules._createTypeRule(factory);
factory.deserialize = function (data, deserializer) {
hashAllTypes();
return deserialize(this, JSON.parse(data), typeMap, {
deserializer: deserializer
});
};
let rule = typeRules._createTypeRule(factory);
registeredTypes.add(rule);
registeredTypesDirty = true;
return rule;
}
};

@ -65,7 +65,9 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
let validator = rule._validator;
baseRule = function (value) {
if (validator.call(this, value) === true) {
if (value._isDeserializationPlaceholder === true) {
return true;
} else if (validator.call(this, value) === true) {
return true;
} else {
return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`);
@ -77,33 +79,39 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
baseRule = function (value) {
/* FIXME: Better error when the type is unknown */
let actualType = rule._registry._types.get(rule._name);
if (value._isDeserializationPlaceholder === true) {
return true;
} else {
let actualType = rule._registry._getType(rule._name);
if (actualType._isCustomType) {
if (actualType._validator.call(this, value) === true) {
return true;
if (actualType._isCustomType) {
if (actualType._validator.call(this, value) === true) {
return true;
} else {
return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`);
}
} else {
return new errors.ValidationError(`Expected ${getValueType(rule)}, got ${getValueType(value)} instead`);
}
} else {
if (aliasValidator == null) {
aliasValidator = generateValidator(actualType._alias, name);
}
if (aliasValidator == null) {
aliasValidator = generateValidator(actualType._alias, name);
}
let validatorResult = aliasValidator.call(this, value);
let validatorResult = aliasValidator.call(this, value);
if (validatorResult === true) {
return true;
} else {
// return new errors.ValidationError(`Expected ${getValueType(actualType._alias)}, got ${getValueType(value)} instead`);
/* FIXME: Possible this is done wrong elsewhere too, swallowing the actual validation error? */
return validatorResult;
if (validatorResult === true) {
return true;
} else {
// return new errors.ValidationError(`Expected ${getValueType(actualType._alias)}, got ${getValueType(value)} instead`);
/* FIXME: Possible this is done wrong elsewhere too, swallowing the actual validation error? */
return validatorResult;
}
}
}
};
} else if (rule._isTrait === true) {
baseRule = function (value) {
if (value._type != null && value._type._implementedTraits != null && value._type._implementedTraits.has(rule)) {
if (value._isDeserializationPlaceholder === true) {
return true;
} else 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`);
@ -111,14 +119,18 @@ module.exports = function generateValidator(rule, name = "<unknown>") {
};
} else if (rule._isRegistryTrait === true) {
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 = rule._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)) {
if (value._isDeserializationPlaceholder === true) {
return true;
} else {
return new errors.ValidationError(`Expected object of a type with the ${rule._name} trait, got ${getValueType(value)} instead`);
/* 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 = rule._registry._getTrait(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) {

@ -0,0 +1,49 @@
"use strict";
module.exports = function jsonStringifyPreReplacer(data, replacer = ((key, value) => value)) {
function stringifyPiece(parent, key, node, level) {
let replacedNode = replacer(key, node);
let serializedNode;
if (replacedNode != null && typeof replacedNode.toJSON === "function") {
serializedNode = replacedNode.toJSON();
} else {
serializedNode = replacedNode;
}
if (serializedNode === undefined) {
return;
} else if (typeof serializedNode !== "object" || serializedNode === null) {
return JSON.stringify(serializedNode);
} else if (Array.isArray(serializedNode)) {
let stringifiedItems = serializedNode.map((item, i) => {
let stringifiedItem = stringifyPiece(serializedNode, i, item, level + 1);
if (stringifiedItem === undefined) {
return null;
} else {
return stringifiedItem;
}
});
return `[${stringifiedItems.join(",")}]`;
} else {
let stringifiedItems = Object.entries(serializedNode).map(([key, value]) => {
let stringifiedValue = stringifyPiece(serializedNode, key, value, level + 1);
if (stringifiedValue !== undefined) {
return `${JSON.stringify(key)}:${stringifiedValue}`;
} else {
return undefined;
}
}).filter((item) => {
return (item !== undefined);
});
return `{${stringifiedItems.join(",")}}`;
}
}
return stringifyPiece({"": data}, "", data, 0);
};

@ -9,6 +9,20 @@ module.exports = function createRegistry() {
return Object.assign({}, moduleAPI, {
_types: new Map(),
_traits: new Map(),
_getType: function (name) {
if (this._types.has(name)) {
return this._types.get(name);
} else {
throw new Error(`No type named ${name} was found in the registry`);
}
},
_getTrait: function (name) {
if (this._traits.has(name)) {
return this._traits.get(name);
} else {
throw new Error(`No trait named ${name} was found in the registry`);
}
},
createType: function (name, schema, options = {}) {
if (!this._types.has(name)) {
let type = moduleAPI.createType(name, schema, options);

@ -0,0 +1,151 @@
"use strict";
const defaultValue = require("default-value");
const mapObj = require("map-obj");
const isPlaceholder = require("../util/is-placeholder");
const createPlaceholderManager = require("./placeholder-manager");
function isReference(serializedData) {
return (serializedData != null && serializedData._d_sT === "cTR");
}
module.exports = function createDeserializer(topLevelType, topLevelData, typeMap, options = {}) {
let seen = new Map();
let placeholders = createPlaceholderManager();
let customDeserializer = defaultValue(options.deserializer, (value) => value);
function deserializeSet(rule, entries) {
let seenPlaceholder = false;
let set = new Set(entries.map((item) => {
let deserializedValue = deserializeEntry(rule._itemType, item);
if (isPlaceholder(deserializedValue)) {
seenPlaceholder = true;
}
return deserializedValue;
}));
if (seenPlaceholder) {
placeholders.markSet(set);
}
return set;
}
function deserializeMap(rule, entries) {
let seenPlaceholder = false;
let map = new Map(entries.map(([itemKey, itemValue]) => {
let deserializedKey = deserializeEntry(rule._keyType, itemKey);
let deserializedValue = deserializeEntry(rule._itemType, itemValue);
if (isPlaceholder(deserializedKey) || isPlaceholder(deserializedValue)) {
seenPlaceholder = true;
}
return [
deserializedKey,
deserializedValue
];
}));
if (seenPlaceholder) {
placeholders.markMap(map);
}
return map;
}
function deserializeEntry(rule, serializedData) {
if (rule._isCustomType) {
return deserializeInstanceOrReference(rule, serializedData);
} else if (rule._isCustomRegistryType) {
return deserializeInstanceOrReference(rule._registry._getType(rule._name), serializedData);
} else if (rule._isRegistryTrait || rule._isTrait) {
return deserializeInstanceFuzzy(serializedData);
} else if (rule._collectionType === "set") {
if (serializedData._d_sT === "gS") {
return deserializeSet(rule, serializedData.entries);
} else {
/* FIXME: Clearer error message */
throw new Error("Expected guarded set, but got something else instead");
}
} else if (rule._collectionType === "map") {
if (serializedData._d_sT === "gM") {
return deserializeMap(rule, serializedData.entries);
} else {
/* FIXME: Clearer error message */
throw new Error("Expected guarded map, but got something else instead");
}
} else {
/* This also covers `either` rules where the serializedData is a type instance or reference. */
return deserializeEntryFuzzy(serializedData);
}
}
function deserializeEntryFuzzy(serializedData) {
if (serializedData != null && (serializedData._d_sT === "cT" || serializedData._d_sT === "cTR")) {
return deserializeInstanceFuzzy(serializedData);
} else {
return customDeserializer(serializedData);
}
}
function deserializeInstance(typeFactory, serializedData) {
let placeholderProperties = [];
let deserializedData = mapObj(serializedData.data, (key, value) => {
let rule = typeFactory._cumulativeSchema[key];
let deserializedValue = deserializeEntry(rule, value);
if (isPlaceholder(deserializedValue)) {
placeholderProperties.push(key);
}
return [key, deserializedValue];
});
let instance = typeFactory(deserializedData);
for (let property of placeholderProperties) {
placeholders.markProperty(instance, property);
}
seen.set(serializedData.id, instance);
return instance;
}
function deserializeReference(serializedData) {
return {
_isDeserializationPlaceholder: true,
_deserializationId: serializedData.id
};
}
function deserializeInstanceOrReference(typeFactory, serializedData) {
if (isReference(serializedData)) {
return deserializeReference(serializedData);
} else {
return deserializeInstance(typeFactory, serializedData);
}
}
function deserializeInstanceFuzzy(serializedData) {
if (isReference(serializedData)) {
return deserializeReference(serializedData);
} else if (typeMap.has(serializedData.type)) {
return deserializeInstance(typeMap.get(serializedData.type), serializedData);
} else {
throw new Error(`Encountered instance of an unknown type with hash ${serializedData.type}`);
}
}
let topLevelInstance = deserializeInstance(topLevelType, topLevelData);
placeholders.fillInPlaceholders(seen);
return topLevelInstance;
};

@ -0,0 +1,94 @@
"use strict";
const isPlaceholder = require("../util/is-placeholder");
module.exports = function createPlaceholderManager() {
let placeholders = [];
return {
markProperty: function (object, property) {
placeholders.push({
type: "property",
object: object,
property: property
});
},
markSet: function (set) {
placeholders.push({
type: "set",
set: set
});
},
markMap: function (map) {
placeholders.push({
type: "map",
map: map
});
},
fillInPlaceholders: function (seen) {
function getSeenItem(placeholder) {
if (seen.has(placeholder._deserializationId)) {
return seen.get(placeholder._deserializationId);
} else {
throw new Error(`Did not encounter an object with the ID ${placeholder._deserializationId}`);
}
}
function fillInSetPlaceholders(set) {
let originalItems = Array.from(set.values());
set.clear();
for (let item of originalItems) {
if (isPlaceholder(item)) {
set.add(getSeenItem(item));
} else {
set.add(item);
}
}
}
function fillInMapPlaceholders(map) {
let originalItems = Array.from(map.entries());
map.clear();
for (let [key, value] of originalItems) {
let newKey, newValue;
if (isPlaceholder(key)) {
newKey = getSeenItem(key);
} else {
newKey = key;
}
if (isPlaceholder(value)) {
newValue = getSeenItem(value);
} else {
newValue = value;
}
map.set(newKey, newValue);
}
}
function fillInPlaceholderProperty(object, property) {
object[property] = getSeenItem(object[property]);
}
placeholders.forEach((marker) => {
/* NOTE: The Set/Map implementations below are probably slow. Maybe there's a faster implementation? */
if (marker.type === "set") {
fillInSetPlaceholders(marker.set);
} else if (marker.type === "map") {
fillInMapPlaceholders(marker.map);
} else if (marker.type === "property") {
fillInPlaceholderProperty(marker.object, marker.property);
} else {
throw new Error(`Unrecognized placeholder marker type: ${marker.type}`);
}
});
/* To ensure that any accidental future invocations of this function will fail loudly. */
placeholders = null;
}
};
};

@ -0,0 +1,52 @@
"use strict";
const defaultValue = require("default-value");
const nanoid = require("nanoid");
const jsonStringifyPreReplacer = require("../json-stringify-pre-replacer");
module.exports = function serialize(instance, options = {}) {
let customSerializer = defaultValue(options.serializer, (value) => value);
/* FIXME: Remove `serializer` argument and let this be specified on fields / type aliases instead? */
let seenItems = new Map();
let data = jsonStringifyPreReplacer(instance, (key, value) => {
if (value == null) {
return value;
} else if (value._guardedCollectionType === "set") {
return {
_d_sT: "gS",
entries: Array.from(value._source.values())
};
} else if (value._guardedCollectionType === "map") {
return {
_d_sT: "gM",
entries: Array.from(value._source.entries())
};
} else if (value._type != null && value._type._isCustomType === true) {
if (!seenItems.has(value)) {
let objectID = nanoid();
seenItems.set(value, objectID);
value._type._ensureHash();
return {
_d_sT: "cT",
type: value._type._typeHash,
data: value._data,
id: objectID
};
} else {
return {
_d_sT: "cTR",
id: seenItems.get(value)
};
}
} else {
return customSerializer(value);
}
});
return data;
};

@ -0,0 +1,166 @@
"use strict";
// tag
/* FIXME: How are validator callbacks handled? */
const mapObj = require("map-obj");
const objectHash = require("object-hash");
const filterTypeRules = require("./util/filter-type-rules");
function hashType(type, level = 0, seenTypes = new Map()) {
/* Type hashes should always be fully local; that is, they are calculated entirely from the point of view of the top-level type being hashed, with respect to how circular type references are handled. This means that we may effectively be calculating a hash for a type more than once, but it's necessary to ensure determinism; if we didn't do this and globally cached hashes for each type definition, then switching around the definition order of types could cause rippling hash changes throughout every referenced type, because circular type references are handled by recording the relative location of the first-seen instance of a type. */
function canonicalRule(rule, level) {
let props;
if (rule == null) {
return rule;
} else if (rule._baseType != null) {
let options;
if (rule._baseType === "guardedFunction") {
options = {
args: rule._options.args.map((arg) => {
return canonicalRule(arg, level + 1);
}),
returnType: canonicalRule(rule._options.returnType, level + 1)
};
} else {
options = rule._options;
}
props = {
ruleType: "baseType",
baseType: rule._baseType,
options: options
};
} else if (rule._collectionType != null) {
props = {
ruleType: "collection",
keyType: canonicalRule(rule._keyType, level + 1),
itemType: canonicalRule(rule._itemType, level + 1),
options: rule._options
};
} else if (rule._modifierType != null) {
if (rule._modifierType === "either") {
props = {
ruleType: "either",
types: rule._types.map((type) => {
return canonicalRule(type, level + 1);
})
};
} else {
throw new Error(`Encountered unexpected modifier: ${rule._modifierType}`);
}
} else if (rule._isSlotRule) {
props = {
ruleType: "slot"
};
} else if (rule._isSelfRule) {
props = {
ruleType: "self"
};
} else if (rule._constructorType != null) {
props = {
ruleType: "instance",
constructor: rule._constructorType
};
} else if (rule._isCustomType || rule._isCustomRegistryType) {
let actualType;
if (rule._isCustomRegistryType) {
actualType = rule._registry._getType(rule._name);
} else {
actualType = rule;
}
let seenLevel = seenTypes.get(actualType);
/* We ignore same-level seen types, because those are sibling properties on the same type. Duplication there is acceptable. */
if (seenLevel != null && seenLevel < level) {
props = {
ruleType: "typeReference",
levels: level - seenTypes.get(actualType)
};
} else {
seenTypes.set(actualType, level);
props = {
ruleType: "type",
typeHash: hashType(actualType, level + 1, seenTypes)
};
}
} else if (rule._isTrait || rule._isRegistryTrait) {
let actualTrait;
if (rule._isRegistryTrait) {
actualTrait = rule._registry._getTrait(rule._name);
} else {
actualTrait = rule;
}
let seenLevel = seenTypes.get(actualTrait);
if (seenLevel != null && seenLevel < level) {
props = {
ruleType: "traitReference",
levels: level - seenTypes.get(actualTrait)
};
} else {
seenTypes.set(actualTrait, level);
props = {
ruleType: "trait",
typeHash: hashType(actualTrait, level + 1, seenTypes)
};
}
} else {
throw new Error("Foo");
}
for (let [seenRule, seenLevel] of seenTypes) {
if (seenLevel > level) {
seenTypes.delete(seenRule);
}
}
return Object.assign(props, {
/* FIXME: Do processing to allow instances of types as default value? */
constraints: rule._constraints
});
// return Object.assign(props, {
// constraints: rule._constraints.map((constraint) => {
// return canonicalRule(constraint, level + 1);
// })
// });
}
seenTypes.set(type, level);
/* FIXME: Constraints for custom types / traits? */
if (type._isCustomType) {
return mapObj(filterTypeRules(type._cumulativeSchema), (key, value) => {
return [key, canonicalRule(value, level)];
});
} else if (type._isTrait) {
return mapObj(filterTypeRules(type._schema), (key, value) => {
return [key, canonicalRule(value, level)];
});
} else if (type._isTypeAlias) {
return {
_ruleType: "typeAlias",
type: canonicalRule(type._alias)
};
} else {
throw new Error("Can only hash types and traits");
}
}
module.exports = function generateTypeHash(type) {
return objectHash(hashType(type), {
algorithm: "sha256",
respectFunctionNames: false,
respectFunctionProperties: false
}).slice(0, 6);
};

@ -0,0 +1,9 @@
"use strict";
const filterObj = require("filter-obj");
module.exports = function filterTypeRules(schema) {
return filterObj(schema, (key, value) => {
return (value != null && value._isTypeRule === true);
});
};

@ -0,0 +1,5 @@
"use strict";
module.exports = function isPlaceholder(value) {
return (value != null && value._isDeserializationPlaceholder);
};

@ -5,6 +5,7 @@ const expect = require("chai").expect;
const dm = require("../src");
/* FIXME: setOf/mapOf? */
/* FIXME: Type aliases? Both producing and consuming */
describe("registry", () => {
let registry = dm.createRegistry();

@ -0,0 +1,94 @@
"use strict";
const expect = require("chai").expect;
const dm = require("../src");
const generateTypeHash = require("../src/type-hash");
function customSerializer(value) {
if (value instanceof Date) {
return {
_d_sT: "date",
date: Math.round(value.getTime() / 1000)
};
} else {
return value;
}
}
function customDeserializer(value) {
if (value != null && value._d_sT === "date") {
let date = new Date();
date.setTime(value.date);
return date;
} else {
return value;
}
}
let registry = dm.createRegistry();
let User = registry.createType("User", {
name: dm.string(),
messages: dm.setOf(registry.trait("Addressable"))
});
let Addressable = registry.createTrait("Addressable", {
to: registry.type("User"),
from: registry.type("User"),
});
let Message = registry.createType("Message", {
body: dm.string(),
date: dm.instanceOf(Date)
}).implements(Addressable, {
to: dm.slot(),
from: dm.slot()
});
describe("serialization", () => {
it("should serialize correctly", () => {
let joe = User({
name: "Joe",
messages: new Set()
});
let jane = User({
name: "Jane",
messages: new Set()
});
let messageOne = Message({
to: joe,
from: jane,
body: "Hello world!",
date: new Date()
});
joe.messages.add(messageOne);
let messageTwo = Message({
to: jane,
from: joe,
body: "Hello you!",
date: new Date()
});
jane.messages.add(messageTwo);
let serializedJoe = joe.serialize(customSerializer);
let deserializedJoe = User.deserialize(serializedJoe, customDeserializer);
expect(joe.name).to.equal(deserializedJoe.name);
expect(Array.from(joe.messages)[0].body).to.equal(Array.from(deserializedJoe.messages)[0].body);
expect(Array.from(joe.messages)[0].from.name).to.equal(Array.from(deserializedJoe.messages)[0].from.name);
let serializedMessageTwo = messageTwo.serialize(customSerializer);
let deserializedMessageTwo = Message.deserialize(serializedMessageTwo, customDeserializer);
expect(messageTwo.body).to.equal(deserializedMessageTwo.body);
expect(messageTwo.to.name).to.equal(deserializedMessageTwo.to.name);
expect(messageTwo.from.name).to.equal(deserializedMessageTwo.from.name);
expect(Array.from(messageTwo.from.messages)[0].body).to.equal(Array.from(deserializedMessageTwo.from.messages)[0].body);
});
});
Loading…
Cancel
Save