Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] context for serialization #67

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions serializr.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ export interface Context {
parentContext: Context;
args: any;
await(modelschema: ClazzOrModelSchema<any>,id:string,callback?: (err: any, result: any) => void):any;
rootContext:Context;
rootContext: Context;
addCallback: (callbackFunction: (error?: Error) => void) => void;
}

export interface SerializeContext {
parentContext: SerializeContext;
rootContext: SerializeContext;
addCallback: (callbackFunction: (error?: Error) => void) => void;
}

export type Factory<T> = (context: Context) => T
Expand Down Expand Up @@ -45,6 +52,9 @@ export function serialize<T>(instance: T): any;
export function deserialize<T>(modelschema: ClazzOrModelSchema<T>, jsonArray: any[], callback?: (err: any, result: T[]) => void, customArgs?: any): T[];
export function deserialize<T>(modelschema: ClazzOrModelSchema<T>, json: any, callback?: (err: any, result: T) => void, customArgs?: any): T;

export function deserializeObjectWithSchema<T>(parentContext: Context, modelschema: ClazzOrModelSchema<T>, jsonArray: any[], callback?: (err: any, result: T[]) => void, customArgs?: any): T[];
export function deserializeObjectWithSchema<T>(parentContext: Context, modelschema: ClazzOrModelSchema<T>, json: any, callback?: (err: any, result: T) => void, customArgs?: any): T;

export function update<T>(modelschema: ClazzOrModelSchema<T>, instance:T, json: any, callback?: (err: any, result: T) => void, customArgs?: any): void;
export function update<T>(instance:T, json: any, callback?: (err: any, result: T) => void, customArgs?: any): void;

Expand Down Expand Up @@ -73,11 +83,12 @@ export function map(propSchema: PropSchema): PropSchema;

export function mapAsArray(propSchema: PropSchema, keyPropertyName: string): PropSchema;

export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context?: any, oldValue?: any) => any): PropSchema;
export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => any): PropSchema;
export function custom(serializer: (value: any, sourcePropertyName?: string, sourceObject?: any, context?: SerializeContext) => any, deserializer: (jsonValue: any, context?: any, oldValue?: any) => any): PropSchema;
export function custom(serializer: (value: any, sourcePropertyName?: string, sourceObject?: any, context?: SerializeContext) => any, deserializer: (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => any): PropSchema;

export function serializeAll<T extends Function>(clazz: T): T

export function getIdentifierProperty(modelSchema: ClazzOrModelSchema<any>): string;
export const SKIP: {}
export function raw(): any;

export const SKIP: {}
41 changes: 41 additions & 0 deletions src/core/BaseContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

import { GUARDED_NOOP } from "../utils/utils";

export function BaseContext(parentContext, onReadyCb) {
this.parentContext = parentContext
this.onFinishCallbacks = [];
this.onReadyCb = onReadyCb || GUARDED_NOOP;
this.error = undefined;
this.rootContext = this.parentContext && this.parentContext.rootContext || this;
this.isRoot = !this.parentContext;
}

BaseContext.prototype.addCallback = function addCallback(listenerFunction) {
if (typeof listenerFunction === "function") {
this.onFinishCallbacks = this.onFinishCallbacks || [];
this.onFinishCallbacks.push(listenerFunction);
}
};

BaseContext.prototype.setError = function setError(newError) {
this.error = newError;
};

BaseContext.prototype.finished = function finished(data) {

if (this.onFinishCallbacks && Array.isArray(this.onFinishCallbacks) && this.onFinishCallbacks.length > 0) {
var _this = this;
this.onFinishCallbacks.forEach(function (listener) {
if (typeof listener === "function") {
listener(_this.error);
}
})
}
if (typeof this.onReadyCb === "function") {
if (this.error) {
this.onReadyCb(this.error);
} else {
this.onReadyCb(undefined, data);
}
}
}
41 changes: 20 additions & 21 deletions src/core/Context.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,48 @@
import { GUARDED_NOOP, once, invariant, isAssignableTo } from "../utils/utils"
import { once, invariant, isAssignableTo } from "../utils/utils"
import { BaseContext } from "./BaseContext";

export default function Context(parentContext, modelSchema, json, onReadyCb, customArgs) {
this.parentContext = parentContext
this.isRoot = !parentContext
BaseContext.call(this, parentContext, onReadyCb);

this.pendingCallbacks = 0
this.pendingRefsCount = 0
this.onReadyCb = onReadyCb || GUARDED_NOOP
this.json = json
this.target = null
this.hasError = false
this.modelSchema = modelSchema
if (this.isRoot) {
this.rootContext = this
this.args = customArgs
this.pendingRefs = {} // uuid: [{ modelSchema, uuid, cb }]
this.resolvedRefs = {} // uuid: [{ modelSchema, value }]
} else {
this.rootContext = parentContext.rootContext
} else if (parentContext) {
this.args = parentContext.args
}
}

Context.prototype = new BaseContext();

Context.prototype.createCallback = function (fn) {
this.pendingCallbacks++
// once: defend against user-land calling 'done' twice
return once(function(err, value) {
if (err) {
if (!this.hasError) {
this.hasError = true
this.onReadyCb(err)
this.setError(err);
this.finished();
}
} else if (!this.hasError) {
fn(value)
if (--this.pendingCallbacks === this.pendingRefsCount) {
if (this.pendingRefsCount > 0)
// all pending callbacks are pending reference resolvers. not good.
this.onReadyCb(new Error(
"Unresolvable references in json: \"" +
Object.keys(this.pendingRefs).filter(function (uuid) {
return this.pendingRefs[uuid].length > 0
}, this).join("\", \"") +
"\""
))
else
this.onReadyCb(null, this.target)
if (this.pendingRefsCount > 0) {
// all pending callbacks are pending reference resolvers. not good.
this.setError(new Error(
"Unresolvable references in json: \"" +
Object.keys(this.pendingRefs).filter(function (uuid) {
return this.pendingRefs[uuid].length > 0
}, this).join("\", \"") +
"\""
));
}
this.finished(this.target);
}
}
}.bind(this))
Expand Down
8 changes: 8 additions & 0 deletions src/core/SerializeContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BaseContext } from "./BaseContext";

export default function SerializeContext(parentContext, modelSchema, json, onReadyCb, customArgs) {
BaseContext.call(this, parentContext, onReadyCb);

}

SerializeContext.prototype = new BaseContext();
33 changes: 24 additions & 9 deletions src/core/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import createModelSchema from "../api/createModelSchema"
import getDefaultModelSchema from "../api/getDefaultModelSchema"
import setDefaultModelSchema from "../api/setDefaultModelSchema"
import { SKIP, _defaultPrimitiveProp } from "../constants"
import SerializeContext from "./SerializeContext";

/**
* Serializes an object (graph) into json using the provided model schema.
Expand All @@ -13,8 +14,8 @@ import { SKIP, _defaultPrimitiveProp } from "../constants"
* @param arg2 object(s) to serialize
* @returns {object} serialized representation of the object
*/
export default function serialize(arg1, arg2) {
invariant(arguments.length === 1 || arguments.length === 2, "serialize expects one or 2 arguments")
export default function serialize(arg1, arg2, parentContext) {
invariant(arguments.length >= 1 && arguments.length <= 3, "serialize expects one or 2 arguments")
var thing = arguments.length === 1 ? arg1 : arg2
var schema = arguments.length === 1 ? null : arg1
if (Array.isArray(thing)) {
Expand All @@ -26,40 +27,54 @@ export default function serialize(arg1, arg2) {
schema = getDefaultModelSchema(thing)
}
invariant(!!schema, "Failed to find default schema for " + arg1)
if (Array.isArray(thing))
return thing.map(function (item) {
return serializeWithSchema(schema, item)

var context = new SerializeContext(parentContext, schema)
var result
if (Array.isArray(thing)) {
result = thing.map(function (item) {
return serializeWithSchemaAndContext(context, schema, item)
})
return serializeWithSchema(schema, thing)
} else {
result = serializeWithSchemaAndContext(context, schema, thing);
}
context.finished(result);
return result;
}

export function serializeWithSchema(schema, obj) {
return serializeWithSchemaAndContext(null, schema, obj);
}

function serializeWithSchemaAndContext(parentContext, schema, obj) {
invariant(schema && typeof schema === "object", "Expected schema")
invariant(obj && typeof obj === "object", "Expected object")
var res
if (schema.extends)
res = serializeWithSchema(schema.extends, obj)
res = serializeWithSchemaAndContext(parentContext, schema.extends, obj)
else {
// TODO: make invariant?: invariant(!obj.constructor.prototype.constructor.serializeInfo, "object has a serializable supertype, but modelschema did not provide extends clause")
res = {}
}

var context = new SerializeContext(parentContext, schema, res)
Object.keys(schema.props).forEach(function (key) {
var propDef = schema.props[key]
if (key === "*") {
invariant(propDef === true, "prop schema '*' can onle be used with 'true'")
invariant(propDef === true, "prop schema '*' can only be used with 'true'")
serializeStarProps(schema, obj, res)
return
}
if (propDef === true)
propDef = _defaultPrimitiveProp
if (propDef === false)
return
var jsonValue = propDef.serializer(obj[key], key, obj)
var jsonValue = propDef.serializer(obj[key], key, obj, context)
if (jsonValue === SKIP){
return
}
res[propDef.jsonname || key] = jsonValue
})
context.finished(res);
return res
}

Expand Down
3 changes: 2 additions & 1 deletion src/serializr.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export { default as serializable } from "./api/serializable"
* ## Serialization and deserialization
*/
export { default as serialize, serializeAll } from "./core/serialize"
export { default as deserialize } from "./core/deserialize"
export { default as deserialize, deserializeObjectWithSchema } from "./core/deserialize"
export { default as update } from "./core/update"

export { default as primitive } from "./types/primitive"
Expand All @@ -74,6 +74,7 @@ export { default as map } from "./types/map"
export { default as mapAsArray } from "./types/mapAsArray"
export { default as raw } from "./types/raw"

export { getIdentifierProp as getIdentifierProperty } from "./utils/utils";
export { SKIP } from "./constants"

// deprecated
Expand Down
2 changes: 1 addition & 1 deletion src/types/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import {invariant} from "../utils/utils"
* };

*
* @param {function} serializer function that takes a model value and turns it into a json value
* @param {function} serializer function that takes a model value and turns it into a json value. It also takes context argument, which can allow you to add a global callback to the ending of serialization.
* @param {function} deserializer function that takes a json value and turns it into a model value. It also takes context argument, which can allow you to deserialize based on the context of other parameters.
* @returns {PropSchema}
*/
Expand Down
4 changes: 2 additions & 2 deletions src/types/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export default function list(propSchema) {
invariant(isPropSchema(propSchema), "expected prop schema as first argument")
invariant(!isAliasedPropSchema(propSchema), "provided prop is aliased, please put aliases first")
return {
serializer: function (ar) {
serializer: function (ar, key, target, context) {
invariant(ar && "length" in ar && "map" in ar, "expected array (like) object")
return ar.map(propSchema.serializer)
return ar.map(function (item, index) { return propSchema.serializer(item, index, ar, context) })
},
deserializer: function(jsonArray, done, context) {
if (!Array.isArray(jsonArray))
Expand Down
6 changes: 3 additions & 3 deletions src/types/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ export default function map(propSchema) {
invariant(isPropSchema(propSchema), "expected prop schema as first argument")
invariant(!isAliasedPropSchema(propSchema), "provided prop is aliased, please put aliases first")
return {
serializer: function (m) {
serializer: function (m, k, target, context) {
invariant(m && typeof m === "object", "expected object or Map")
var isMap = isMapLike(m)
var result = {}
if (isMap)
m.forEach(function(value, key) {
result[key] = propSchema.serializer(value)
result[key] = propSchema.serializer(value, key, m, context)
})
else for (var key in m)
result[key] = propSchema.serializer(m[key])
result[key] = propSchema.serializer(m[key], key, m, context)
return result
},
deserializer: function(jsonObject, done, context, oldValue) {
Expand Down
4 changes: 2 additions & 2 deletions src/types/mapAsArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export default function mapAsArray(propSchema, keyPropertyName) {
invariant(isPropSchema(propSchema), "expected prop schema as first argument")
invariant(!!keyPropertyName, "expected key property name as second argument")
return {
serializer: function (m) {
serializer: function (m, k, target, context) {
var result = []
// eslint-disable-next-line no-unused-vars
m.forEach(function (value, key) {
result.push(propSchema.serializer(value))
result.push(propSchema.serializer(value, key, m, context))
})
return result
},
Expand Down
4 changes: 2 additions & 2 deletions src/types/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import { deserializeObjectWithSchema } from "../core/deserialize"
export default function object(modelSchema) {
invariant(typeof modelSchema === "object" || typeof modelSchema === "function", "No modelschema provided. If you are importing it from another file be aware of circular dependencies.")
return {
serializer: function (item) {
serializer: function (item, key, target, context) {
modelSchema = getDefaultModelSchema(modelSchema)
invariant(isModelSchema(modelSchema), "expected modelSchema, got " + modelSchema)
if (item === null || item === undefined)
return item
return serialize(modelSchema, item)
return serialize(modelSchema, item, context)
},
deserializer: function (childJson, done, context) {
modelSchema = getDefaultModelSchema(modelSchema)
Expand Down
17 changes: 17 additions & 0 deletions test/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ test("it should pass context to custom schemas", t => {
t.end()
})

test("it should pass context to custom schemas and provide an internal callback with serialization", t => {
var callbackWasCalled = false;
var s = _.createSimpleSchema({
a: _.custom(
function(v, k, obj, context) {
context.rootContext.addCallback(function () {callbackWasCalled = true})
return v + 2
},
function(v) { return v - 2 }
)
})
t.deepEqual(_.serialize(s, { a: 4 }), { a: 6 })
t.equal(callbackWasCalled, true, "internal serialization callback was not called")
t.deepEqual(_.deserialize(s, { a: 6 }), { a: 4 })
t.end()
})

test("it should respect extends", t => {
var superSchema = _.createSimpleSchema({
x: primitive()
Expand Down