Initial serialization implementation
parent
b630797895
commit
d0079c6bac
@ -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);
|
||||
};
|
@ -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);
|
||||
};
|
@ -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…
Reference in New Issue