diff --git a/CHANGELOG.md b/CHANGELOG.md index 902d1c9..2e7c0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * **BREAKING CHANGE**: `{"*": object(...)}` and `{"x": object(...)}` now behave the same when deserializing `{"x": "str_not_object"}` (result: `{x: null}`). Previously the `"*"` schema would have returned `{}`. +* **BREAKING CHANGE**: Removed deprecated `ref` and `child` functions. Use `reference` and `object` + instead. * You can pass a `pattern` argument as `AdditionalPropArgs` instead of having to manually assign it to a PropSchema: `@serializeAll("*": list(primitive(), { pattern: /^_.*/ }))`. Note this only make sense together with the `"*"` property. diff --git a/src/api/createModelSchema.ts b/src/api/createModelSchema.ts index 23e3c65..1889ff0 100644 --- a/src/api/createModelSchema.ts +++ b/src/api/createModelSchema.ts @@ -2,6 +2,7 @@ import { invariant } from "../utils/utils" import getDefaultModelSchema from "./getDefaultModelSchema" import setDefaultModelSchema from "./setDefaultModelSchema" import { ModelSchema, Clazz, Props, Factory, ClazzOrModelSchema } from "./types" +import object from "../types/object" /** * Creates a model schema that (de)serializes an object created by a constructor function (class). @@ -49,6 +50,6 @@ export default function createModelSchema( if (s && s.targetClass !== clazz) model.extends = s } setDefaultModelSchema(clazz, model) - return model + return object(model) } const x: Clazz = Object diff --git a/src/api/createSimpleSchema.ts b/src/api/createSimpleSchema.ts index b6da9d3..cd7d061 100644 --- a/src/api/createSimpleSchema.ts +++ b/src/api/createSimpleSchema.ts @@ -1,4 +1,5 @@ import { Props, ModelSchema } from "./types" +import object from "../types/object" /** * Creates a model schema that (de)serializes from / to plain javascript objects. @@ -17,10 +18,10 @@ import { Props, ModelSchema } from "./types" * @returns model schema */ export default function createSimpleSchema(props: Props): ModelSchema { - return { + return object({ factory: function() { return {} as any }, props: props - } + }) } diff --git a/src/api/serializable.ts b/src/api/serializable.ts index 8a2ddaa..098fade 100644 --- a/src/api/serializable.ts +++ b/src/api/serializable.ts @@ -1,9 +1,9 @@ -import { invariant, isPropSchema, isAliasedPropSchema } from "../utils/utils" +import { invariant, isSchema, isAliasedSchema } from "../utils/utils" import { _defaultPrimitiveProp } from "../constants" import primitive from "../types/primitive" import getDefaultModelSchema from "../api/getDefaultModelSchema" import createModelSchema from "../api/createModelSchema" -import { PropSchema, ModelSchema, PropDef } from "./types" +import { Schema, ModelSchema, PropDef } from "./types" import Context from "../core/Context" // Ugly way to get the parameter names since they aren't easily retrievable via reflection @@ -16,7 +16,7 @@ function getParamNames(func: Function) { } function serializableDecorator( - propSchema: PropSchema, + propSchema: Schema, target: any, propName: string, descriptor: PropertyDescriptor | undefined @@ -34,8 +34,8 @@ function serializableDecorator( descriptor !== undefined && typeof descriptor === "number" ) { - invariant(isPropSchema(propSchema), "Constructor params must use alias(name)") - invariant(isAliasedPropSchema(propSchema), "Constructor params must use alias(name)") + invariant(isSchema(propSchema), "Constructor params must use alias(name)") + invariant(isAliasedSchema(propSchema), "Constructor params must use alias(name)") const paramNames = getParamNames(target) if (paramNames.length >= descriptor) { propName = paramNames[descriptor] @@ -46,12 +46,12 @@ function serializableDecorator( factory = function(context: Context) { const params: any = [] for (let i = 0; i < target.constructor.length; i++) { - Object.keys(context.modelSchema.props).forEach(function(key) { + for (const key of Object.keys(context.modelSchema.props)) { const prop = context.modelSchema.props[key] - if ((prop as PropSchema).paramNumber === i) { - params[i] = context.json[(prop as PropSchema).jsonname!] + if ((prop as Schema).paramNumber === i) { + params[i] = context.json[(prop as Schema).jsonname!] } - }) + } } return target.constructor.bind(undefined, ...params) @@ -105,15 +105,15 @@ export default function serializable( baseDescriptor?: PropertyDescriptor ): void export default function serializable( - targetOrPropSchema: any | PropDef, + targetOrSchema: any | PropDef, key?: string, baseDescriptor?: PropertyDescriptor ) { if (!key) { // decorated with propSchema const propSchema = - targetOrPropSchema === true ? _defaultPrimitiveProp : (targetOrPropSchema as PropSchema) - invariant(isPropSchema(propSchema), "@serializable expects prop schema") + targetOrSchema === true ? _defaultPrimitiveProp : (targetOrSchema as Schema) + invariant(isSchema(propSchema), "@serializable expects prop schema") const result: ( target: Object, key: string, @@ -122,6 +122,6 @@ export default function serializable( return result } else { // decorated without arguments, treat as primitive - serializableDecorator(primitive(), targetOrPropSchema, key, baseDescriptor!) + serializableDecorator(primitive(), targetOrSchema, key, baseDescriptor!) } } diff --git a/src/api/types.ts b/src/api/types.ts index 0e987aa..9b38270 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -8,48 +8,47 @@ export interface AdditionalPropArgs { } export type PropSerializer = ( sourcePropertyValue: any, - key: string | number | symbol, + key: string | number | symbol | undefined, sourceObject: any ) => any | SKIP export type PropDeserializer = ( jsonValue: any, - callback: (err?: any, targetPropertyValue?: any | SKIP) => void, + callback: (err?: any, newValue?: any | SKIP) => void, context: Context, - currentPropertyValue?: any + currentPropertyValue?: any, + customArg?: any ) => void -export interface PropSchema { - serializer: PropSerializer - deserializer: PropDeserializer - beforeDeserialize?: BeforeDeserializeFunc - afterDeserialize?: AfterDeserializeFunc - /** - * Filter properties to which this schema applies. Used with `ModelSchema.props["*"]`. - */ - pattern?: { test: (propName: string) => boolean } - jsonname?: string - identifier?: true - paramNumber?: number -} - export type AfterDeserializeFunc = ( callback: (err: any, value: any) => void, err: any, newValue: any, jsonValue: any, jsonParentValue: any, - propNameOrIndex: string | number | symbol, + jsonPropNameOrIndex: string | number | undefined, context: Context, - propDef: PropSchema + propDef: Schema ) => void - export type BeforeDeserializeFunc = ( callback: (err: any, value: any) => void, jsonValue: any, jsonParentValue: any, - propNameOrIndex: string | number, + propNameOrIndex: string | number | undefined, context: Context, - propDef: PropSchema + propDef: Schema ) => void +export interface Schema { + serializer: PropSerializer + deserializer: PropDeserializer + beforeDeserialize?: BeforeDeserializeFunc + afterDeserialize?: AfterDeserializeFunc + /** + * Filter properties to which this schema applies. Used with `ModelSchema.props["*"]`. + */ + pattern?: { test: (propName: string) => boolean } + jsonname?: string + identifier?: true + paramNumber?: number +} export type Factory = (context: Context) => T @@ -60,9 +59,9 @@ export type Factory = (context: Context) => T export type Props = { [propName in keyof T]: PropDef } -export type PropDef = PropSchema | boolean | undefined +export type PropDef = Schema | boolean | undefined -export interface ModelSchema { +export interface ModelSchema extends Schema { targetClass?: Clazz factory: Factory props: Props diff --git a/src/core/Context.ts b/src/core/Context.ts index 0421499..7a88b45 100644 --- a/src/core/Context.ts +++ b/src/core/Context.ts @@ -4,13 +4,10 @@ import { ModelSchema } from "../api/types" const rootContextCache = new WeakMap() export default class Context { - private isRoot: boolean private pendingCallbacks: number private pendingRefsCount: number public target: any private hasError: boolean - public rootContext: Context - private args: any private pendingRefs!: { [uuid: string]: { modelSchema: ModelSchema @@ -26,29 +23,19 @@ export default class Context { } constructor( - readonly parentContext: Context | undefined, - readonly modelSchema: ModelSchema, readonly json: any, private readonly onReadyCb: (err?: any, value?: T) => void, - customArgs?: any[] + private readonly args?: any ) { - this.isRoot = !parentContext this.pendingCallbacks = 0 this.pendingRefsCount = 0 this.target = undefined // always set this property using setTarget this.hasError = false - if (!parentContext) { - this.rootContext = this - this.args = customArgs - this.pendingRefs = {} - this.resolvedRefs = {} - } else { - this.rootContext = parentContext.rootContext - this.args = parentContext.args - } + this.pendingRefs = {} + this.resolvedRefs = {} } - createCallback(fn: (value: T) => void) { + createCallback(fn: (value: any) => void) { this.pendingCallbacks++ // once: defend against user-land calling 'done' twice return once((err?: any, value?: T) => { @@ -85,7 +72,6 @@ export default class Context { // given an object with uuid, modelSchema, callback, awaits until the given uuid is available // resolve immediately if possible await(modelSchema: ModelSchema, uuid: string, callback: (err?: any, value?: any) => void) { - invariant(this.isRoot, "await can only be called on the root context") if (uuid in this.resolvedRefs) { const match = this.resolvedRefs[uuid].filter(function(resolved) { return isAssignableTo(resolved.modelSchema, modelSchema) @@ -103,7 +89,6 @@ export default class Context { // given a model schema, uuid and value, resolve all references that were looking for this object resolve(modelSchema: ModelSchema, uuid: string, value: any) { - invariant(this.isRoot, "resolve can only called on the root context") if (!this.resolvedRefs[uuid]) this.resolvedRefs[uuid] = [] this.resolvedRefs[uuid].push({ modelSchema: modelSchema, @@ -123,7 +108,7 @@ export default class Context { // set target and update root context cache setTarget(target: T) { - if (this.isRoot && this.target) { + if (this.target) { rootContextCache.delete(this.target) } this.target = target @@ -132,7 +117,6 @@ export default class Context { // call all remaining reference lookup callbacks indicating an error during ref resolution cancelAwaits() { - invariant(this.isRoot, "cancelAwaits can only be called on the root context") const self = this Object.keys(this.pendingRefs).forEach(function(uuid) { self.pendingRefs[uuid].forEach(function(refOpts) { diff --git a/src/core/deserialize.ts b/src/core/deserialize.ts index 0b13627..97d7244 100644 --- a/src/core/deserialize.ts +++ b/src/core/deserialize.ts @@ -9,53 +9,11 @@ import { ClazzOrModelSchema, AfterDeserializeFunc, BeforeDeserializeFunc, - PropSchema, + Schema, ModelSchema, PropDef } from "../api/types" -function schemaHasAlias(schema: ModelSchema, name: string) { - for (const key in schema.props) { - const propSchema = schema.props[key] - if (typeof propSchema === "object" && propSchema.jsonname === name) return true - } - return false -} - -function deserializeStarProps( - context: Context, - schema: ModelSchema, - propDef: PropDef, - obj: any, - json: any -) { - for (const key in json) - if (!(key in schema.props) && !schemaHasAlias(schema, key)) { - const 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 && (!propDef.pattern || propDef.pattern.test(key))) { - 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(r => r !== SKIP && (obj[key] = r)), - context - ) - } - } -} - /** * Deserializes a json structure into an object graph. * @@ -95,178 +53,74 @@ export default function deserialize( const schema = getDefaultModelSchema(clazzOrModelSchema) invariant(isModelSchema(schema), "first argument should be model schema") if (Array.isArray(json)) { - const items: any[] = [] + const result: any[] = new Array(json.length) parallel( json, - function(childJson, itemDone) { - const instance = deserializeObjectWithSchema( - undefined, - schema, - childJson, - itemDone, - customArgs - ) + (childJson, itemDone, idx) => { + const instance = deserializeWithSchema(schema, childJson, itemDone, customArgs) // instance is created synchronously so can be pushed - items.push(instance) + result[idx] = instance }, callback ) - return items + return result } else { - return deserializeObjectWithSchema(undefined, schema, json, callback, customArgs) + let result: T | T[] = undefined! + deserializeWithSchema( + schema, + json, + (err, value) => ((result = value), callback(err, value)), + customArgs + ) + return result } } -export function deserializeObjectWithSchema( - parentContext: Context | undefined, - modelSchema: ModelSchema, - json: any, +export function deserializeWithSchema( + schema: Schema, + jsonValue: any, callback: (err?: any, value?: any) => void, customArgs: any ) { - if (json === null || json === undefined || typeof json !== "object") - return void callback(null, null) - const context = new Context(parentContext, modelSchema, json, callback, customArgs) - const target = modelSchema.factory(context) - // todo async invariant - invariant(!!target, "No object returned from factory") - // TODO: make invariant? invariant(schema.extends || - // !target.constructor.prototype.constructor.serializeInfo, "object has a serializable - // supertype, but modelschema did not provide extends clause") - context.setTarget(target) - const lock = context.createCallback(GUARDED_NOOP) - deserializePropsWithSchema(context, modelSchema, json, target) - lock() - return target + const context = new Context(jsonValue, callback, customArgs) + doDeserialize(callback, jsonValue, undefined, undefined, context, schema) } - -export function deserializePropsWithSchema( - context: Context, - modelSchema: ModelSchema, - json: any, - target: T +export function doDeserialize( + callback: (err?: any, value?: any) => void, + jsonValue: any, + jsonParentValue: any, + jsonPropNameOrIndex: number | string | undefined, + context: Context, + schema: Schema ) { - if (modelSchema.extends) deserializePropsWithSchema(context, modelSchema.extends, json, target) - - function deserializeProp(propDef: PropSchema, jsonValue: object, propName: keyof T) { - function preProcess(resultCallback: (err: any, result: any) => void) { - return function(err: any, newValue: any) { - function finalCallback(errPreliminary: any, finalOrRetryValue: any) { - if ( - errPreliminary && - finalOrRetryValue !== undefined && - typeof propDef.afterDeserialize === "function" - ) { - propDef.deserializer( - finalOrRetryValue, - preProcess(resultCallback), - context, - target[propName] - ) - } else { - resultCallback(errPreliminary, finalOrRetryValue) - } - } - - onAfterDeserialize( - finalCallback, - err, - newValue, - jsonValue, - json, - propName, - context, - propDef - ) - } - } - - 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 - preProcess( - context.rootContext.createCallback(r => r !== SKIP && (target[propName] = r)) - ), - context, - target[propName] // initial value + const serialize: (err: any, value: any) => void = (err, preprocessedJsonValue) => + schema.deserializer( + preprocessedJsonValue, + schema.afterDeserialize + ? (err, newValue) => + schema.afterDeserialize!( + callback, + err, + newValue, + preprocessedJsonValue, + jsonParentValue, + jsonPropNameOrIndex, + context, + schema + ) + : callback, + context ) - } - - for (const key of Object.keys(modelSchema.props) as (keyof T)[]) { - let propDef: PropDef = modelSchema.props[key] - if (!propDef) return - - if (key === "*") { - deserializeStarProps(context, modelSchema, propDef, target, json) - return - } - if (propDef === true) propDef = _defaultPrimitiveProp - const jsonAttr = propDef.jsonname ?? key - invariant("symbol" !== typeof jsonAttr, "You must alias symbol properties. prop = %l", key) - const jsonValue = json[jsonAttr] - const propSchema = propDef - const callbackDeserialize = (err: any, jsonValue: any) => { - if (!err && jsonValue !== undefined) { - deserializeProp(propSchema, jsonValue, key) - } - } - onBeforeDeserialize( - callbackDeserialize, - jsonValue, - json, - jsonAttr as string | number, - context, - propDef - ) - } -} - -export const onBeforeDeserialize: BeforeDeserializeFunc = ( - callback, - jsonValue, - jsonParentValue, - propNameOrIndex, - context, - propDef -) => { - if (propDef && typeof propDef.beforeDeserialize === "function") { - propDef.beforeDeserialize( - callback, - jsonValue, - jsonParentValue, - propNameOrIndex, - context, - propDef - ) - } else { - callback(null, jsonValue) - } -} - -export const onAfterDeserialize: AfterDeserializeFunc = ( - callback, - err, - newValue, - jsonValue, - jsonParentValue, - propNameOrIndex, - context, - propDef -) => { - if (propDef && typeof propDef.afterDeserialize === "function") { - propDef.afterDeserialize( - callback, - err, - newValue, + if (schema.beforeDeserialize) { + schema.beforeDeserialize( + serialize, jsonValue, jsonParentValue, - propNameOrIndex, + jsonPropNameOrIndex, context, - propDef + schema ) } else { - callback(err, newValue) + serialize(undefined, jsonValue) } } diff --git a/src/core/serialize.ts b/src/core/serialize.ts index 2df7b35..d33e6cb 100644 --- a/src/core/serialize.ts +++ b/src/core/serialize.ts @@ -1,7 +1,7 @@ import { invariant, isPrimitive } from "../utils/utils" import getDefaultModelSchema from "../api/getDefaultModelSchema" import { SKIP, _defaultPrimitiveProp } from "../constants" -import { ClazzOrModelSchema, PropSchema, ModelSchema, PropDef } from "../api/types" +import { ClazzOrModelSchema, Schema, ModelSchema, PropDef } from "../api/types" /** * Serializes an object (graph) into json using the provided model schema. @@ -33,53 +33,7 @@ export default function serialize(modelSchemaOrInstance: ClazzOrModelSchema serializeWithSchema(foundSchema, item)) - return serializeWithSchema(foundSchema, instance) -} - -function serializeWithSchema(schema: ModelSchema, obj: any): T { - invariant(schema && typeof schema === "object" && schema.props, "Expected schema") - invariant(obj && typeof obj === "object", "Expected object") - let res: any - if (schema.extends) res = serializeWithSchema(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 = {} - } - Object.keys(schema.props).forEach(function(key) { - let propDef: PropDef = schema.props[key as keyof T] - if (!propDef) return - if (key === "*") { - serializeStarProps(schema, propDef, obj, res) - return - } - if (propDef === true) propDef = _defaultPrimitiveProp - const jsonValue = propDef.serializer(obj[key], key, obj) - if (jsonValue === SKIP) { - return - } - res[propDef.jsonname || key] = jsonValue - }) - return res -} - -function serializeStarProps(schema: ModelSchema, propDef: PropDef, obj: any, target: any) { - for (const key of Object.keys(obj)) - if (!(key in schema.props)) { - if (propDef === true || (propDef && (!propDef.pattern || propDef.pattern.test(key)))) { - const value = obj[key] - if (propDef === true) { - if (isPrimitive(value)) { - target[key] = value - } - } else { - const jsonValue = propDef.serializer(value, key, obj) - if (jsonValue === SKIP) { - return - } - // TODO: propDef.jsonname could be a transform function on key - target[key] = jsonValue - } - } - } + if (Array.isArray(instance)) + return instance.map((item, index) => foundSchema.serializer(item, index, instance)) + return foundSchema.serializer(instance, undefined, undefined) } diff --git a/src/core/serializeAll.ts b/src/core/serializeAll.ts index 825e29f..494b23a 100644 --- a/src/core/serializeAll.ts +++ b/src/core/serializeAll.ts @@ -1,4 +1,4 @@ -import { invariant, isPropSchema } from "../utils/utils" +import { invariant, isSchema } from "../utils/utils" import createModelSchema from "../api/createModelSchema" import getDefaultModelSchema from "../api/getDefaultModelSchema" import setDefaultModelSchema from "../api/setDefaultModelSchema" @@ -64,7 +64,7 @@ export default function serializeAll( if (true === propertyType) { propertyType = _defaultPrimitiveProp } - invariant(isPropSchema(propertyType), "couldn't resolve schema") + invariant(isSchema(propertyType), "couldn't resolve schema") propSchema = Object.assign({}, propertyType, { pattern: targetOrPattern }) diff --git a/src/core/update.ts b/src/core/update.ts index 3e8f097..0c80d5c 100644 --- a/src/core/update.ts +++ b/src/core/update.ts @@ -5,8 +5,9 @@ import { invariant, isModelSchema, GUARDED_NOOP } from "../utils/utils" import getDefaultModelSchema from "../api/getDefaultModelSchema" import Context from "./Context" -import { deserializePropsWithSchema } from "./deserialize" import { ClazzOrModelSchema } from "../api/types" +import { deserializePropsWithSchema } from "../types/object" +import { doDeserialize } from "./deserialize" /** * Similar to deserialize, but updates an existing object instance. @@ -36,7 +37,7 @@ export function update( export default function update( modelSchema: any, target: any, - json: any, + jsonValue: any, callback: any, customArgs?: any ) { @@ -45,7 +46,7 @@ export default function update( if (inferModelSchema) { target = arguments[0] modelSchema = getDefaultModelSchema(target) - json = arguments[1] + jsonValue = arguments[1] callback = arguments[2] customArgs = arguments[3] } else { @@ -56,10 +57,10 @@ export default function update( typeof target === "object" && target && !Array.isArray(target), "update needs an object" ) - const context = new Context(undefined, modelSchema, json, callback || GUARDED_NOOP, customArgs) + const context = new Context(modelSchema, jsonValue, callback || GUARDED_NOOP, customArgs) context.setTarget(target) const lock = context.createCallback(GUARDED_NOOP) - const result = deserializePropsWithSchema(context, modelSchema, json, target) + doDeserialize(callback, jsonValue, undefined, undefined, context, modelSchema) lock() - return result + return target } diff --git a/src/serializr.ts b/src/serializr.ts index fb8c32a..2235239 100644 --- a/src/serializr.ts +++ b/src/serializr.ts @@ -31,8 +31,3 @@ export { default as mapAsArray } from "./types/mapAsArray" export { default as raw } from "./types/raw" export { SKIP } from "./constants" - -// deprecated -export { default as child } from "./types/object" -export { default as ref } from "./types/reference" -// ~ deprecated diff --git a/src/types/alias.ts b/src/types/alias.ts index 832f289..1924f33 100644 --- a/src/types/alias.ts +++ b/src/types/alias.ts @@ -1,11 +1,6 @@ -import { - invariant, - isPropSchema, - isAliasedPropSchema, - isIdentifierPropSchema -} from "../utils/utils" +import { invariant, isSchema, isAliasedSchema, isIdentifierSchema } from "../utils/utils" import { _defaultPrimitiveProp } from "../constants" -import { PropSchema, PropDef } from "../api/types" +import { Schema, PropDef } from "../api/types" /** * Alias indicates that this model property should be named differently in the generated json. @@ -21,16 +16,16 @@ import { PropSchema, PropDef } from "../api/types" * @param name name of the json field to be used for this property * @param propSchema propSchema to (de)serialize the contents of this field */ -export default function alias(name: string, propSchema?: PropDef): PropSchema { +export default function alias(name: string, propSchema?: PropDef): Schema { invariant(name && typeof name === "string", "expected prop name as first argument") propSchema = !propSchema || propSchema === true ? _defaultPrimitiveProp : propSchema - invariant(isPropSchema(propSchema), "expected prop schema as second argument") - invariant(!isAliasedPropSchema(propSchema), "provided prop is already aliased") + invariant(isSchema(propSchema), "expected prop schema as second argument") + invariant(!isAliasedSchema(propSchema), "provided prop is already aliased") return { jsonname: name, serializer: propSchema.serializer, deserializer: propSchema.deserializer, - identifier: isIdentifierPropSchema(propSchema) || undefined, + identifier: isIdentifierSchema(propSchema) || undefined, beforeDeserialize: propSchema.beforeDeserialize, afterDeserialize: propSchema.afterDeserialize } diff --git a/src/types/custom.ts b/src/types/custom.ts index 015ff65..22f937f 100644 --- a/src/types/custom.ts +++ b/src/types/custom.ts @@ -1,5 +1,5 @@ import { invariant, processAdditionalPropArgs } from "../utils/utils" -import { AdditionalPropArgs, PropSchema, PropSerializer } from "../api/types" +import { AdditionalPropArgs, Schema, PropSerializer } from "../api/types" import { SKIP } from "../constants" /** @@ -64,7 +64,7 @@ export default function custom( serializer: PropSerializer, deserializer: (jsonValue: any, context: any, oldValue: any) => any | SKIP, additionalArgs?: AdditionalPropArgs -): PropSchema +): Schema export default function custom( serializer: PropSerializer, deserializer: ( @@ -74,7 +74,7 @@ export default function custom( callback: (err: any, result: any | SKIP) => void ) => void, additionalArgs?: AdditionalPropArgs -): PropSchema +): Schema export default function custom( serializer: PropSerializer, deserializer: @@ -86,10 +86,10 @@ export default function custom( callback: (err: any, result: any | SKIP) => void ) => void), additionalArgs?: AdditionalPropArgs -): PropSchema { +): Schema { invariant(typeof serializer === "function", "first argument should be function") invariant(typeof deserializer === "function", "second argument should be a function or promise") - let result: PropSchema = { + let result: Schema = { serializer: serializer, deserializer: function(jsonValue, done, context, oldValue) { const result = deserializer(jsonValue, context, oldValue, done) diff --git a/src/types/date.ts b/src/types/date.ts index 7661846..176ecd2 100644 --- a/src/types/date.ts +++ b/src/types/date.ts @@ -1,14 +1,14 @@ import { invariant, processAdditionalPropArgs } from "../utils/utils" -import { PropSchema, AdditionalPropArgs } from "../api/types" +import { Schema, AdditionalPropArgs } from "../api/types" /** * Similar to primitive, serializes instances of Date objects * * @param additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers */ -export default function date(additionalArgs?: AdditionalPropArgs): PropSchema { +export default function date(additionalArgs?: AdditionalPropArgs): Schema { // TODO: add format option? - let result: PropSchema = { + let result: Schema = { serializer: function(value) { if (value === null || value === undefined) return value invariant(value instanceof Date, "Expected Date object") diff --git a/src/types/identifier.ts b/src/types/identifier.ts index 5de4157..5353220 100644 --- a/src/types/identifier.ts +++ b/src/types/identifier.ts @@ -1,10 +1,6 @@ import { invariant, processAdditionalPropArgs } from "../utils/utils" import { _defaultPrimitiveProp } from "../constants" -import { AdditionalPropArgs, PropSchema, RegisterFunction } from "../api/types" - -const defaultRegisterFunction: RegisterFunction = (id, value, context) => { - context.rootContext.resolve(context.modelSchema, id, context.target) -} +import { AdditionalPropArgs, Schema, RegisterFunction } from "../api/types" /** * @@ -43,8 +39,8 @@ const defaultRegisterFunction: RegisterFunction = (id, value, context) => { export function identifier( registerFn?: RegisterFunction, additionalArgs?: AdditionalPropArgs -): PropSchema -export function identifier(additionalArgs: AdditionalPropArgs): PropSchema +): Schema +export function identifier(additionalArgs: AdditionalPropArgs): Schema export default function identifier( arg1?: RegisterFunction | AdditionalPropArgs, arg2?: AdditionalPropArgs @@ -61,20 +57,11 @@ export default function identifier( !additionalArgs || typeof additionalArgs === "object", "Additional property arguments should be an object, register function should be omitted or a funtion" ) - let result: PropSchema = { + let result: Schema = { identifier: true, + // registerFn, serializer: _defaultPrimitiveProp.serializer, - deserializer: function(jsonValue, done, context) { - _defaultPrimitiveProp.deserializer( - jsonValue, - function(err, id) { - defaultRegisterFunction(id, context.target, context) - if (registerFn) registerFn(id, context.target, context) - done(err, id) - }, - context - ) - } + deserializer: _defaultPrimitiveProp.deserializer } result = processAdditionalPropArgs(result, additionalArgs) return result diff --git a/src/types/list.ts b/src/types/list.ts index 6fab5d7..379ffa8 100644 --- a/src/types/list.ts +++ b/src/types/list.ts @@ -1,14 +1,14 @@ import { SKIP } from "../constants" import { invariant, - isPropSchema, - isAliasedPropSchema, + isSchema, + isAliasedSchema, parallel, processAdditionalPropArgs } from "../utils/utils" -import { onAfterDeserialize, onBeforeDeserialize } from "../core/deserialize" import { _defaultPrimitiveProp } from "../constants" -import { AdditionalPropArgs, PropSchema } from "../api/types" +import { AdditionalPropArgs, Schema } from "../api/types" +import { doDeserialize } from "../core/deserialize" /** * List indicates that this property contains a list of things. @@ -39,17 +39,11 @@ import { AdditionalPropArgs, PropSchema } from "../api/types" * @param propSchema to be used to (de)serialize the contents of the array * @param additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers */ -export default function list( - propSchema: PropSchema, - additionalArgs?: AdditionalPropArgs -): PropSchema { +export default function list(propSchema: Schema, additionalArgs?: AdditionalPropArgs): Schema { propSchema = propSchema || _defaultPrimitiveProp - invariant(isPropSchema(propSchema), "expected prop schema as first argument") - invariant( - !isAliasedPropSchema(propSchema), - "provided prop is aliased, please put aliases first" - ) - let result: PropSchema = { + invariant(isSchema(propSchema), "expected prop schema as first argument") + invariant(!isAliasedSchema(propSchema), "provided prop is aliased, please put aliases first") + let result: Schema = { serializer: function(ar) { if (ar === undefined) { return SKIP @@ -60,59 +54,12 @@ export default function list( deserializer: function(jsonArray, done, context) { if (!Array.isArray(jsonArray)) return void done("[serializr] expected JSON array") - function processItem( - jsonValue: any, - onItemDone: (err?: any, value?: any) => void, - itemIndex: number - ) { - function callbackBefore(err: any, value: any) { - if (!err) { - propSchema.deserializer(value, deserializeDone, context) - } else { - onItemDone(err) - } - } - - function deserializeDone(err: any, value: any) { - if (typeof propSchema.afterDeserialize === "function") { - onAfterDeserialize( - callbackAfter, - err, - value, - jsonValue, - jsonArray, - itemIndex, - context, - propSchema - ) - } else { - onItemDone(err, value) - } - } - - function callbackAfter(errPreliminary: any, finalOrRetryValue: any) { - if ( - errPreliminary && - finalOrRetryValue !== undefined && - typeof propSchema.afterDeserialize === "function" - ) { - propSchema.deserializer(finalOrRetryValue, deserializeDone, context) - } else { - onItemDone(errPreliminary, finalOrRetryValue) - } - } - - onBeforeDeserialize( - callbackBefore, - jsonValue, - jsonArray, - itemIndex, - context, - propSchema - ) - } - - parallel(jsonArray, processItem, done) + parallel( + jsonArray, + (jsonValue, onItemDone, itemIndex) => + doDeserialize(onItemDone, jsonValue, jsonArray, itemIndex, context, propSchema), + done + ) } } result = processAdditionalPropArgs(result, additionalArgs) diff --git a/src/types/map.ts b/src/types/map.ts index 94c0906..348cf58 100644 --- a/src/types/map.ts +++ b/src/types/map.ts @@ -1,13 +1,13 @@ import { invariant, - isAliasedPropSchema, - isPropSchema, + isAliasedSchema, + isSchema, isMapLike, processAdditionalPropArgs } from "../utils/utils" import { _defaultPrimitiveProp } from "../constants" import list from "./list" -import { PropSchema, AdditionalPropArgs } from "../api/types" +import { Schema, AdditionalPropArgs } from "../api/types" /** * Similar to list, but map represents a string keyed dynamic collection. @@ -16,17 +16,11 @@ import { PropSchema, AdditionalPropArgs } from "../api/types" * * @param additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers */ -export default function map( - propSchema: PropSchema, - additionalArgs?: AdditionalPropArgs -): PropSchema { +export default function map(propSchema: Schema, additionalArgs?: AdditionalPropArgs): Schema { propSchema = propSchema || _defaultPrimitiveProp - invariant(isPropSchema(propSchema), "expected prop schema as first argument") - invariant( - !isAliasedPropSchema(propSchema), - "provided prop is aliased, please put aliases first" - ) - let result: PropSchema = { + invariant(isSchema(propSchema), "expected prop schema as first argument") + invariant(!isAliasedSchema(propSchema), "provided prop is aliased, please put aliases first") + let result: Schema = { serializer: function(m: Map | { [key: string]: any }) { invariant(m && typeof m === "object", "expected object or Map") const result: { [key: string]: any } = {} diff --git a/src/types/mapAsArray.ts b/src/types/mapAsArray.ts index c921282..5b53d94 100644 --- a/src/types/mapAsArray.ts +++ b/src/types/mapAsArray.ts @@ -1,7 +1,7 @@ -import { invariant, isPropSchema, isMapLike, processAdditionalPropArgs } from "../utils/utils" +import { invariant, isSchema, isMapLike, processAdditionalPropArgs } from "../utils/utils" import { _defaultPrimitiveProp } from "../constants" import list from "./list" -import { PropSchema, AdditionalPropArgs } from "../api/types" +import { Schema, AdditionalPropArgs } from "../api/types" /** * Similar to map, mapAsArray can be used to serialize a map-like collection where the key is @@ -16,14 +16,14 @@ import { PropSchema, AdditionalPropArgs } from "../api/types" * @param additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers */ export default function mapAsArray( - propSchema: PropSchema, + propSchema: Schema, keyPropertyName: string, additionalArgs?: AdditionalPropArgs -): PropSchema { +): Schema { propSchema = propSchema || _defaultPrimitiveProp - invariant(isPropSchema(propSchema), "expected prop schema as first argument") + invariant(isSchema(propSchema), "expected prop schema as first argument") invariant(!!keyPropertyName, "expected key property name as second argument") - let result: PropSchema = { + let result: Schema = { serializer: function(m) { invariant(m && typeof m === "object", "expected object or Map") const result = [] diff --git a/src/types/object.ts b/src/types/object.ts index a9cc2f9..4cd1c73 100644 --- a/src/types/object.ts +++ b/src/types/object.ts @@ -1,8 +1,24 @@ -import { invariant, isModelSchema, processAdditionalPropArgs } from "../utils/utils" +import { + invariant, + isModelSchema, + processAdditionalPropArgs, + GUARDED_NOOP, + isPrimitive +} from "../utils/utils" import getDefaultModelSchema from "../api/getDefaultModelSchema" import serialize from "../core/serialize" -import { deserializeObjectWithSchema } from "../core/deserialize" -import { ClazzOrModelSchema, AdditionalPropArgs, PropSchema } from "../api/types" +import { ClazzOrModelSchema, AdditionalPropArgs, Schema, ModelSchema, PropDef } from "../api/types" +import Context from "../core/Context" +import { doDeserialize } from "../core/deserialize" +import { _defaultPrimitiveProp, SKIP } from "../constants" + +function schemaHasAlias(schema: ModelSchema, name: string) { + for (const key in schema.props) { + const propSchema = schema.props[key] + if (typeof propSchema === "object" && propSchema.jsonname === name) return true + } + return false +} /** * `object` indicates that this property contains an object that needs to be (de)serialized @@ -35,31 +51,154 @@ import { ClazzOrModelSchema, AdditionalPropArgs, PropSchema } from "../api/types export default function object( modelSchema: ClazzOrModelSchema, additionalArgs?: AdditionalPropArgs -): PropSchema { +): Schema { invariant( typeof modelSchema === "object" || typeof modelSchema === "function", "No modelschema provided. If you are importing it from another file be aware of circular dependencies." ) - let result: PropSchema = { + let result: Schema = { serializer: function(item) { modelSchema = getDefaultModelSchema(modelSchema)! invariant(isModelSchema(modelSchema), "expected modelSchema, got " + modelSchema) if (item === null || item === undefined) return item - return serialize(modelSchema, item) + return serializePropsWithSchema(modelSchema, item) }, - deserializer: function(childJson, done, context) { + deserializer: function(jsonValue, callback, context, currentPropValue, customArg) { modelSchema = getDefaultModelSchema(modelSchema)! - invariant(isModelSchema(modelSchema), "expected modelSchema, got " + modelSchema) - if (childJson === null || childJson === undefined) return void done(null, childJson) - return void deserializeObjectWithSchema( - context, - modelSchema, - childJson, - done, - undefined - ) + + if (jsonValue === null || jsonValue === undefined || typeof jsonValue !== "object") + return void callback(null, null) + const target = modelSchema.factory(context) + // todo async invariant + invariant(!!target, "No object returned from factory") + // TODO: make invariant? invariant(schema.extends || + // !target.constructor.prototype.constructor.serializeInfo, "object has a serializable + // supertype, but modelschema did not provide extends clause") + const lock = context.createCallback(GUARDED_NOOP) + deserializePropsWithSchema(context, modelSchema, jsonValue, target) + lock() + return target } } + result = Object.assign(modelSchema, result) result = processAdditionalPropArgs(result, additionalArgs) return result } + +function serializePropsWithSchema(schema: ModelSchema, obj: any): T { + invariant(schema && typeof schema === "object" && schema.props, "Expected schema") + invariant(obj && typeof obj === "object", "Expected object") + let res: any + if (schema.extends) res = serializePropsWithSchema(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 = {} + } + Object.keys(schema.props).forEach(function(key) { + let propDef: PropDef = schema.props[key as keyof T] + if (!propDef) return + if (key === "*") { + serializeStarProps(schema, propDef, obj, res) + return + } + if (propDef === true) propDef = _defaultPrimitiveProp + const jsonValue = propDef.serializer(obj[key], key, obj) + if (jsonValue === SKIP) { + return + } + res[propDef.jsonname || key] = jsonValue + }) + return res +} + +function serializeStarProps(schema: ModelSchema, propDef: PropDef, obj: any, target: any) { + for (const key of Object.keys(obj)) + if (!(key in schema.props)) { + if (propDef === true || (propDef && (!propDef.pattern || propDef.pattern.test(key)))) { + const value = obj[key] + if (propDef === true) { + if (isPrimitive(value)) { + target[key] = value + } + } else { + const jsonValue = propDef.serializer(value, key, obj) + if (jsonValue === SKIP) { + return + } + // TODO: propDef.jsonname could be a transform function on key + target[key] = jsonValue + } + } + } +} + +function deserializeStarProps( + context: Context, + schema: ModelSchema, + propDef: PropDef, + obj: any, + json: any +) { + for (const key in json) + if (!(key in schema.props) && !schemaHasAlias(schema, key)) { + const 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 && (!propDef.pattern || propDef.pattern.test(key))) { + doDeserialize( + context.createCallback(r => r !== SKIP && (obj[key] = r)), + jsonValue, + json, + key, + context, + schema + ) + } + } +} +export function deserializePropsWithSchema( + context: Context, + modelSchema: ModelSchema, + json: any, + target: any +) { + if (modelSchema.extends) deserializePropsWithSchema(context, modelSchema.extends, json, target) + + for (const key of Object.keys(modelSchema.props) as (keyof T)[]) { + let propDef: PropDef = modelSchema.props[key] + if (!propDef) return + + if (key === "*") { + deserializeStarProps(context, modelSchema, propDef, target, json) + return + } + if (propDef === true) propDef = _defaultPrimitiveProp + const jsonAttr = propDef.jsonname ?? key + invariant("symbol" !== typeof jsonAttr, "You must alias symbol properties. prop = %l", key) + const jsonValue = json[jsonAttr] + const propSchema = propDef + doDeserialize( + context.createCallback(r => { + if (r === SKIP) return + target[key] = r + if (propSchema.identifier) { + context.resolve(modelSchema, r, target) + } + }), + jsonValue, + json, + jsonAttr as string | number, + context, + propSchema + ) + } +} diff --git a/src/types/optional.ts b/src/types/optional.ts index 01f7322..5b190c1 100644 --- a/src/types/optional.ts +++ b/src/types/optional.ts @@ -1,6 +1,6 @@ -import { invariant, isPropSchema } from "../utils/utils" +import { invariant, isSchema } from "../utils/utils" import { _defaultPrimitiveProp, SKIP } from "../constants" -import { PropSchema } from "../api/types" +import { Schema } from "../api/types" /** * Optional indicates that this model property shouldn't be serialized if it isn't present. @@ -14,15 +14,15 @@ import { PropSchema } from "../api/types" * * @param propSchema propSchema to (de)serialize the contents of this field */ -export default function optional(propSchema?: PropSchema | boolean): PropSchema { +export default function optional(propSchema?: Schema | boolean): Schema { propSchema = !propSchema || propSchema === true ? _defaultPrimitiveProp : propSchema - invariant(isPropSchema(propSchema), "expected prop schema as second argument") + invariant(isSchema(propSchema), "expected prop schema as second argument") const propSerializer = propSchema.serializer invariant( typeof propSerializer === "function", "expected prop schema to have a callable serializer" ) - const serializer: PropSchema["serializer"] = (sourcePropertyValue, key, sourceObject) => { + const serializer: Schema["serializer"] = (sourcePropertyValue, key, sourceObject) => { const result = propSerializer(sourcePropertyValue, key, sourceObject) if (result === undefined) { return SKIP diff --git a/src/types/primitive.ts b/src/types/primitive.ts index 3365dc8..7b26bc9 100644 --- a/src/types/primitive.ts +++ b/src/types/primitive.ts @@ -1,5 +1,5 @@ import { invariant, isPrimitive, processAdditionalPropArgs } from "../utils/utils" -import { PropSchema, AdditionalPropArgs } from "../api/types" +import { Schema, AdditionalPropArgs } from "../api/types" /** * Indicates that this field contains a primitive value (or Date) which should be serialized literally to json. @@ -13,8 +13,8 @@ import { PropSchema, AdditionalPropArgs } from "../api/types" * * @param additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers */ -export default function primitive(additionalArgs?: AdditionalPropArgs): PropSchema { - let result: PropSchema = { +export default function primitive(additionalArgs?: AdditionalPropArgs): Schema { + let result: Schema = { serializer: function(value) { invariant(isPrimitive(value), "this value is not primitive: " + value) return value diff --git a/src/types/raw.ts b/src/types/raw.ts index 41a77c0..35adc3c 100644 --- a/src/types/raw.ts +++ b/src/types/raw.ts @@ -1,5 +1,5 @@ import { processAdditionalPropArgs } from "../utils/utils" -import { PropSchema, AdditionalPropArgs } from "../api/types" +import { Schema, AdditionalPropArgs } from "../api/types" /** * Indicates that this field is only need to putted in the serialized json or @@ -16,7 +16,7 @@ import { PropSchema, AdditionalPropArgs } from "../api/types" * @param additionalArgs optional object that contains beforeDeserialize and/or afterDeserialize handlers */ export default function raw(additionalArgs: AdditionalPropArgs) { - let result: PropSchema = { + let result: Schema = { serializer: function(value) { return value }, diff --git a/src/types/reference.ts b/src/types/reference.ts index 8830627..32bf875 100644 --- a/src/types/reference.ts +++ b/src/types/reference.ts @@ -9,7 +9,7 @@ import { ClazzOrModelSchema, RefLookupFunction, AdditionalPropArgs, - PropSchema, + Schema, ModelSchema } from "../api/types" import Context from "../core/Context" @@ -20,7 +20,7 @@ function createDefaultRefLookup(modelSchema: ModelSchema) { cb: (err?: any, result?: any) => any, context: Context ) { - context.rootContext.await(modelSchema, uuid, cb) + context.await(modelSchema, uuid, cb) } } @@ -83,16 +83,16 @@ export default function reference( modelSchema: ClazzOrModelSchema, lookupFn?: RefLookupFunction, additionalArgs?: AdditionalPropArgs -): PropSchema +): Schema export default function reference( modelSchema: ClazzOrModelSchema, additionalArgs?: AdditionalPropArgs -): PropSchema +): Schema export default function reference( identifierAttr: string, lookupFn: RefLookupFunction, additionalArgs?: AdditionalPropArgs -): PropSchema +): Schema export default function reference( target: ClazzOrModelSchema | string, lookupFnOrAdditionalPropArgs?: RefLookupFunction | AdditionalPropArgs, @@ -137,7 +137,7 @@ export default function reference( ) } } - let result: PropSchema = { + let result: Schema = { serializer: function(item) { if (!initialized) initialize() return item ? item[childIdentifierAttribute!] : null diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ac79e5d..14d699a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,5 @@ import invariant from "./invariant" -import { ModelSchema, AdditionalPropArgs, PropSchema } from "../api/types" +import { ModelSchema, AdditionalPropArgs, Schema } from "../api/types" export function GUARDED_NOOP(err?: any) { if (err) @@ -56,17 +56,15 @@ export function isModelSchema(thing: any): thing is ModelSchema { return thing && thing.factory && thing.props } -export function isPropSchema(thing: any): thing is PropSchema { +export function isSchema(thing: any): thing is Schema { return thing && thing.serializer && thing.deserializer } -export function isAliasedPropSchema( - propSchema: any -): propSchema is PropSchema & { jsonname: string } { +export function isAliasedSchema(propSchema: any): propSchema is Schema & { jsonname: string } { return typeof propSchema === "object" && "string" == typeof propSchema.jsonname } -export function isIdentifierPropSchema(propSchema: any): propSchema is PropSchema { +export function isIdentifierSchema(propSchema: any): propSchema is Schema { return typeof propSchema === "object" && propSchema.identifier === true } @@ -89,18 +87,18 @@ export function getIdentifierProp(modelSchema: ModelSchema): string | undef let currentModelSchema: ModelSchema | undefined = modelSchema while (currentModelSchema) { for (const propName in currentModelSchema.props) - if (isIdentifierPropSchema(currentModelSchema.props[propName])) return propName + if (isIdentifierSchema(currentModelSchema.props[propName])) return propName currentModelSchema = currentModelSchema.extends } return undefined } -export function processAdditionalPropArgs( +export function processAdditionalPropArgs( propSchema: T, additionalArgs?: AdditionalPropArgs ) { if (additionalArgs) { - invariant(isPropSchema(propSchema), "expected a propSchema") + invariant(isSchema(propSchema), "expected a propSchema") Object.assign(propSchema, additionalArgs) } return propSchema diff --git a/test/simple.js b/test/simple.js index 4c1dcb1..b20ed96 100644 --- a/test/simple.js +++ b/test/simple.js @@ -134,14 +134,14 @@ test("it should respect `*` : true (primitive) prop schemas", t => { }) test("it should respect `*` : schema prop schemas", t => { - var starPropSchema = _.object( + var starSchema = _.object( _.createSimpleSchema({ x: optional(primitive()) }), { pattern: /^\d.\d+$/ } ) - var s = _.createSimpleSchema({ "*": starPropSchema }) + var s = _.createSimpleSchema({ "*": starSchema }) t.deepEqual(_.serialize(s, { "1.0": { x: 42 }, "2.10": { x: 17 } }), { "1.0": { x: 42 }, "2.10": { x: 17 } @@ -157,7 +157,7 @@ test("it should respect `*` : schema prop schemas", t => { t.deepEqual(_.deserialize(s, { "1.0": "not an object" }), { "1.0": null }) var s2 = _.createSimpleSchema({ - "*": starPropSchema, + "*": starSchema, "1.0": _.date() }) t.doesNotThrow(() => _.serialize(s2, { "1.0": new Date(), d: 2 })) @@ -166,7 +166,7 @@ test("it should respect `*` : schema prop schemas", t => { // don't assign aliased attrs var s3 = _.createSimpleSchema({ a: _.alias("1.0", true), - "*": starPropSchema + "*": starSchema }) t.deepEqual(_.deserialize(s3, { b: 4, "1.0": 5, "2.0": { x: 2 } }), { a: 5, "2.0": { x: 2 } })