Skip to content

Commit

Permalink
Merge pull request #105 from pyrogenic/star_schema
Browse files Browse the repository at this point in the history
Support prop schemas for "*" properties
  • Loading branch information
pyrogenic authored Oct 10, 2019
2 parents a9860b6 + 3d37dea commit 6d21f2c
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 51 deletions.
3 changes: 2 additions & 1 deletion serializr.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ export function mapAsArray(propSchema: PropSchema, keyPropertyName: string, addi
export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context?: any, oldValue?: any) => any, additionalArgs?: AdditionalPropArgs): PropSchema;
export function custom(serializer: (value: any) => any, deserializer: (jsonValue: any, context: any, oldValue: any, callback: (err: any, result: any) => void) => any, additionalArgs?: AdditionalPropArgs): PropSchema;

export function serializeAll<T extends Function>(clazz: T): T
export function serializeAll<T>(clazz: Clazz<T>): Clazz<T>;
export function serializeAll(pattern: RegExp, propSchema: PropSchema | true | Function): (clazz: Clazz<any>) => Clazz<any>;

export function raw(): any;

Expand Down
44 changes: 33 additions & 11 deletions src/core/deserialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { invariant, isPrimitive, isModelSchema, parallel, GUARDED_NOOP } from ".
import getDefaultModelSchema from "../api/getDefaultModelSchema"
import { SKIP, _defaultPrimitiveProp } from "../constants"
import Context from "./Context"
import {checkStarSchemaInvariant} from "./serialize";

function schemaHasAlias(schema, name) {
for (var key in schema.props)
Expand All @@ -13,15 +14,38 @@ function schemaHasAlias(schema, name) {
return false
}

function deserializeStarProps(schema, obj, json) {
function deserializeStarProps(context, schema, propDef, obj, json) {
checkStarSchemaInvariant(propDef)
for (var key in json) if (!(key in schema.props) && !schemaHasAlias(schema, key)) {
var value = json[key]
// when deserializing we don't want to silently ignore 'unparseable data' to avoid
// confusing bugs
invariant(isPrimitive(value),
"encountered non primitive value while deserializing '*' properties in property '" +
key + "': " + value)
obj[key] = value
var jsonValue = json[key]
if (propDef === true) {
// when deserializing we don't want to silently ignore 'unparseable data' to avoid
// confusing bugs
invariant(isPrimitive(jsonValue),
"encountered non primitive value while deserializing '*' properties in property '" +
key + "': " + jsonValue)
obj[key] = jsonValue
} else if (propDef.pattern.test(key)) {
if (propDef.factory) {
var resultValue = deserializeObjectWithSchema(context, propDef, jsonValue, context.callback || GUARDED_NOOP, {})
// deserializeObjectWithSchema returns undefined on error
if (resultValue !== undefined) {
obj[key] = resultValue;
}
} else {
function setValue(resultValue) {
if (resultValue !== SKIP) {
obj[key] = resultValue
}
}
propDef.deserializer(jsonValue,
// for individual props, use root context based callbacks
// this allows props to complete after completing the object itself
// enabling reference resolving and such
context.rootContext.createCallback(setValue),
context)
}
}
}
}

Expand Down Expand Up @@ -132,10 +156,8 @@ export function deserializePropsWithSchema(context, modelSchema, json, target) {
deserializeProp(propDef, jsonValue, propName)
}
}

if (propName === "*") {
invariant(propDef === true, "prop schema '*' can only be used with 'true'")
deserializeStarProps(modelSchema, target, json)
deserializeStarProps(context, modelSchema, propDef, target, json)
return
}
if (propDef === true)
Expand Down
67 changes: 30 additions & 37 deletions src/core/serialize.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { invariant, isPrimitive } from "../utils/utils"
import createModelSchema from "../api/createModelSchema"
import getDefaultModelSchema from "../api/getDefaultModelSchema"
import setDefaultModelSchema from "../api/setDefaultModelSchema"
import { SKIP, _defaultPrimitiveProp } from "../constants"

/**
Expand Down Expand Up @@ -37,8 +35,12 @@ export default function serialize(arg1, arg2) {
return serializeWithSchema(schema, thing)
}

export function checkStarSchemaInvariant(propDef) {
invariant(propDef === true || propDef.pattern, `prop schema '*' can only be used with 'true' or a prop def with a 'pattern': ${JSON.stringify(propDef)}`)
}

export function serializeWithSchema(schema, obj) {
invariant(schema && typeof schema === "object", "Expected schema")
invariant(schema && typeof schema === "object" && schema.props, "Expected schema")
invariant(obj && typeof obj === "object", "Expected object")
var res
if (schema.extends)
Expand All @@ -50,8 +52,7 @@ export function serializeWithSchema(schema, obj) {
Object.keys(schema.props).forEach(function (key) {
var propDef = schema.props[key]
if (key === "*") {
invariant(propDef === true, "prop schema '*' can only be used with 'true'")
serializeStarProps(schema, obj, res)
serializeStarProps(schema, propDef, obj, res)
return
}
if (propDef === true)
Expand All @@ -67,38 +68,30 @@ export function serializeWithSchema(schema, obj) {
return res
}

export function serializeStarProps(schema, obj, target) {
export function serializeStarProps(schema, propDef, obj, target) {
checkStarSchemaInvariant(propDef)
for (var key in obj) if (obj.hasOwnProperty(key)) if (!(key in schema.props)) {
var value = obj[key]
// when serializing only serialize primitive props. Assumes other props (without schema) are local state that doesn't need serialization
if (isPrimitive(value))
target[key] = value
}
}

/**
* The `serializeAll` decorator can be used on a class to signal that all primitive properties should be serialized automatically.
*
* @example
* @serializeAll class Store {
* a = 3;
* b;
* }
*
* const store = new Store();
* store.c = 5;
* store.d = {};
* t.deepEqual(serialize(store), { a: 3, b: undefined, c: 5 });
*/
export function serializeAll(target) {
invariant(arguments.length === 1 && typeof target === "function", "@serializeAll can only be used as class decorator")

var info = getDefaultModelSchema(target)
if (!info || !target.hasOwnProperty("serializeInfo")) {
info = createModelSchema(target, {})
setDefaultModelSchema(target, info)
if ((propDef === true) || (propDef.pattern && propDef.pattern.test(key))) {
var value = obj[key]
if (propDef === true) {
if (isPrimitive(value)) {
target[key] = value
}
} else if (propDef.props) {
var jsonValue = serialize(propDef, value)
if (jsonValue === SKIP){
return
}
// todo: propDef.jsonname could be a transform function on key
target[key] = jsonValue
} else {
var jsonValue = propDef.serializer(value, key, obj)
if (jsonValue === SKIP){
return
}
// todo: propDef.jsonname could be a transform function on key
target[key] = jsonValue
}
}
}

getDefaultModelSchema(target).props["*"] = true
return target
}
67 changes: 67 additions & 0 deletions src/core/serializeAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { invariant } from "../utils/utils"
import createModelSchema from "../api/createModelSchema"
import getDefaultModelSchema from "../api/getDefaultModelSchema"
import setDefaultModelSchema from "../api/setDefaultModelSchema"
import object from "../types/object"

/**
* The `serializeAll` decorator can may used on a class to signal that all primitive properties,
* or complex properties with a name matching a `pattern`, should be serialized automatically.
*
* @example
* @serializeAll class Store {
* a = 3;
* b;
* }
*
* const store = new Store();
* store.c = 5;
* store.d = {};
* t.deepEqual(serialize(store), { c: 5 });
*
* @example
* class DataType {
* @serializable
* x;
* @serializable
* y;
* }
* @serializeAll(/^[a-z]$/, DataType) class ComplexStore {
* }
*
* const store = new ComplexStore();
* store.a = {x: 1, y: 2};
* store.b = {};
* store.somethingElse = 5;
* t.deepEqual(serialize(store), { a: {x: 1, y: 2}, b: { x: undefined, y: undefined } });
*/
export default function serializeAll(targetOrPattern, clazzOrSchema) {
let propSchema;
let invokeImmediately = false;
if (arguments.length === 1) {
invariant(typeof targetOrPattern === "function", "@serializeAll can only be used as class decorator");
propSchema = true;
invokeImmediately = true;
}
else {
invariant(typeof targetOrPattern === "object" && targetOrPattern.test, "@serializeAll pattern doesn't have test");
if (typeof clazzOrSchema === "function") {
clazzOrSchema = object(clazzOrSchema);
}
invariant(typeof clazzOrSchema === "object" && clazzOrSchema.serializer, "couldn't resolve schema");
propSchema = Object.assign({}, clazzOrSchema, {pattern: targetOrPattern})
}
function result(target) {
var info = getDefaultModelSchema(target);
if (!info || !target.hasOwnProperty("serializeInfo")) {
info = createModelSchema(target, {});
setDefaultModelSchema(target, info);
}
getDefaultModelSchema(target).props["*"] = propSchema;
return target;
}
if (invokeImmediately) {
return result(targetOrPattern);
}
return result;
}
3 changes: 2 additions & 1 deletion src/serializr.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export { default as serializable } from "./api/serializable"
/*
* ## Serialization and deserialization
*/
export { default as serialize, serializeAll } from "./core/serialize"
export { default as serialize } from "./core/serialize"
export { default as serializeAll } from "./core/serializeAll"
export { default as cancelDeserialize } from "./core/cancelDeserialize"
export { default as deserialize } from "./core/deserialize"
export { default as update } from "./core/update"
Expand Down
37 changes: 36 additions & 1 deletion test/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ test("it should support 'false' and 'true' propSchemas", t => {
t.end()
})

test("it should respect `*` prop schemas", t => {
test("it should respect `*` : true (primitive) prop schemas", t => {
var s = _.createSimpleSchema({ "*" : true })
t.deepEqual(_.serialize(s, { a: 42, b: 17 }), { a: 42, b: 17 })
t.deepEqual(_.deserialize(s, { a: 42, b: 17 }), { a: 42, b: 17 })
Expand Down Expand Up @@ -123,6 +123,41 @@ test("it should respect `*` prop schemas", t => {
t.end()
})

test("it should respect `*` : schema prop schemas", t => {
var schema = Object.assign(_.createSimpleSchema({
x: optional(primitive()),
}), { pattern: /^\d.\d+$/ })

var s = _.createSimpleSchema({ "*" : schema} )
t.deepEqual(_.serialize(s, { "1.0": {x: 42}, "2.10": {x: 17 } }), { "1.0": {x: 42}, "2.10": {x: 17 } })
t.deepEqual(_.deserialize(s, { "1.0": {x: 42}, "2.10": {x: 17 } }), { "1.0": {x: 42}, "2.10": {x: 17 } })

t.deepEqual(_.serialize(s, { a: new Date(), d: 2 }), {})
t.deepEqual(_.serialize(s, { a: {}, "2.10": {x: 17 } }), { "2.10": {x: 17 }})

// deserialize silently ignores properties that aren't objects:
// if (json === null || json === undefined || typeof json !== "object")
// return void callback(null, null)
t.deepEqual(_.deserialize(s, { "1.0": "not an object" }), {})

var s2 = _.createSimpleSchema({
"*" : schema,
"1.0": _.date()
})
t.doesNotThrow(() => _.serialize(s2, { "1.0": new Date(), d: 2 }))
t.deepEqual(_.serialize(s2, { c: {}, "2.0": {x: 2} }), { "1.0": undefined, "2.0": {x: 2} })

// don't assign aliased attrs
var s3 = _.createSimpleSchema({
a: _.alias("1.0", true),
"*" : schema,
})
t.deepEqual(_.deserialize(s3, { b: 4, "1.0": 5, "2.0": {x: 2}}), { a: 5, "2.0": {x: 2}})


t.end()
})

test("it should respect custom schemas", t => {
var s = _.createSimpleSchema({
a: _.custom(
Expand Down
83 changes: 83 additions & 0 deletions test/typescript/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,3 +587,86 @@ test("[ts] @serializeAll", t => {

t.end()
})

test("[ts] @serializeAll(schema)", t => {
class StarValue {
@serializable(optional())
public x?: number;
}

@serializeAll(/^\d\.\d+$/, StarValue)
class StoreWithStarSchema {
[key: string]: StarValue;
}

const store = new StoreWithStarSchema();
store["1.4"] = { x: 1 };
store["1.77"] = { };
(store as any).c = 5;
(store as any).d = {};

t.deepEqual(serialize(store), { "1.4": {x: 1}, "1.77": {} })

const store2 = deserialize(StoreWithStarSchema, { "1.4": {x: 1}, "1.77": {}, c: 4 })
t.deepEqual(store["1.4"], { x: 1 })
t.deepEqual(store["1.77"], { })
t.equal((store2 as any).c, undefined)

t.end()
})

test("[ts] @serializeAll(list schema)", t => {
class StarValue {
@serializable(optional())
public x?: number;
}

@serializeAll(/^\d\.\d+$/, list(object(StarValue)))
class StoreWithStarSchema {
[key: string]: StarValue[];
}

const store = new StoreWithStarSchema();
store["1.4"] = [{ x: 1 }];
store["1.77"] = [{ }];
(store as any).c = 5;
(store as any).d = {};

t.deepEqual(serialize(store), { "1.4": [{x: 1}], "1.77": [{}] })

const store2 = deserialize(StoreWithStarSchema, { "1.4": [{x: 1}], "1.77": [{}], c: 4 })
t.deepEqual(store["1.4"], [{ x: 1 }])
t.deepEqual(store["1.77"], [{ }])
t.equal((store2 as any).c, undefined)

t.end()
})

test("[ts] tests from serializeAll documentation", t => {
@serializeAll class Store {
[key: string]: number;
}

const store = new Store();
store.c = 5;
(store as any).d = {};
t.deepEqual(serialize(store), { c: 5 });

class DataType {
@serializable
x?: number;
@serializable(optional())
y?: number;
}
@serializeAll(/^[a-z]$/, DataType) class ComplexStore {
[key: string]: DataType;
}

const complexStore = new ComplexStore();
complexStore.a = {x: 1, y: 2};
complexStore.b = {};
(complexStore as any).somethingElse = 5;
t.deepEqual(serialize(complexStore), { a: {x: 1, y: 2}, b: { x: undefined } });

t.end();
})

0 comments on commit 6d21f2c

Please sign in to comment.