erossignon/serialijse

View on GitHub
lib/serialijse.js

Summary

Maintainability
C
1 day
Test Coverage
(function (exports) {
    "use strict";
    var assert = require("assert");
    var b = require("buffer");

    var g_classInfos = {};

    // note: phantomjs may not define  Object.assign by default so we need the pony fill
    var objectAssign = Object.assign || require("object-assign");

    var isFunction = function (obj) {
        return typeof obj === 'function' || obj.prototype;
    };


    function merge_options(options1, options2) {
        return objectAssign({}, options1, options2);
    }
    function serializeObject(context, object, rawData, global_options) {

        assert(!rawData.hasOwnProperty("d"));

        rawData.d = {};

        var options = global_options || {};
        if (object.constructor && object.constructor.serialijseOptions) {
            options = merge_options(options, object.constructor.serialijseOptions);
        }
        if (options.ignored) {
            options.ignored = (options.ignored instanceof Array) ? options.ignored : [options.ignored];
        }

        for (var property in object) {
            if (isPropertyPersistable(object, property, options)) {
                if (object[property] !== null) {
                    rawData.d[property] = _serialize(context, options, object[property]);
                } else {
                    rawData.d[property] = null;
                }
            }
        }
    }

    function deserializeObject(context, object_id, object_definition) {

        var classInfo = this;

        var object = new classInfo.constructor();
        context.cache[object_id] = object;

        var rawData = object_definition.d;

        // istanbul ignore next
        if (!rawData) {
            return; // no properties
        }

        for (var property in rawData) {
            if (rawData.hasOwnProperty(property)) {
                try {
                    object[property] = deserialize_node_or_value(context, rawData[property]);
                }
                catch (err)
                // istanbul ignore next
                {
                    console.log(" property : ", property);
                    console.log(err);
                    throw err;
                }
            }
        }

        return object;

    }

    function serializeTypedArray(context, typedArray, rawData) {
        rawData.a = Buffer.from(typedArray.buffer).toString("base64");
    }

    function deserializeTypedArray(context, object_id, rawData) {

        var classInfo = this;
        assert(typeof rawData.a === "string");
        var buf = Buffer.from(rawData.a, "base64");
        var tmp = new Uint8Array(buf);
        var obj = new classInfo.constructor(tmp.buffer);
        context.cache[object_id] = obj;

        return obj;
    }

    function deserialize_node_or_value(context, node) {
        assert(context);
        if ("object" === typeof node) {
            return deserialize_node(context, node);
        }
        return node;
    }

    function declarePersistable(constructor, name, serializeFunc, deserializeFunc) {

        var className = constructor.prototype.constructor.name || constructor.name;

        serializeFunc = serializeFunc || serializeObject;
        deserializeFunc = deserializeFunc || deserializeObject;

        if (name) {
            className = name;
        }

        // istanbul ignore next
        if (g_classInfos.hasOwnProperty(className)) {
            console.warn("declarePersistable warning: declarePersistable : class " + className + " already registered");
        }

        // istanbul ignore next
        if (!(constructor instanceof Function) && !constructor.prototype) {
            throw new Error("declarePersistable: Cannot find constructor for " + className);
        };

        g_classInfos[className] = {
            constructor: constructor,
            serializeFunc: serializeFunc,
            deserializeFunc: deserializeFunc
        };
    }


    function declareTypedArrayPersistable(typeArrayName) {

        // istanbul ignore next
        if (!global[typeArrayName]) {
            console.log("warning : " + typeArrayName + " is not supported in this environment");
            return;
        }

        var constructor = global[typeArrayName];
        // repair constructor name if any
        // istanbul ignore next
        if (!constructor.name) {
            constructor.name = typeArrayName;
        }
        // if (!(constructor instanceof Function)) {
        //     a = new constructor();
        //     throw new Error("warning : " + typeArrayName + " is not supported FULLY FILLY in this environment"   +typeof constructor,typeof constructor.constructor);
        // }
        declarePersistable(constructor, typeArrayName, serializeTypedArray, deserializeTypedArray);

    }
    declarePersistable(Object, "Object", serializeObject, deserializeObject);
    declareTypedArrayPersistable("Float32Array");
    declareTypedArrayPersistable("Float64Array");
    declareTypedArrayPersistable("Uint32Array");
    declareTypedArrayPersistable("Uint16Array");
    declareTypedArrayPersistable("Uint8Array");
    declareTypedArrayPersistable("Int32Array");
    declareTypedArrayPersistable("Int16Array");
    declareTypedArrayPersistable("Int8Array");

    /**
     * returns true if the property is persistable.
     * @param obj
     * @param propertyName
     * @returns {boolean}
     */
    function isPropertyPersistable(obj, propertyName, options) {
        if (!obj.hasOwnProperty(propertyName)) {
            return false;
        }
        if (propertyName === '____index') {
            return false;
        }
        if (options && options.ignored) {

            for (var i = 0; i < options.ignored.length; i++) {
                var o = options.ignored[i];
                if (typeof o === "string") {
                    if (o === propertyName) {
                        return false;
                    }
                } else if (o instanceof RegExp) {
                    if (propertyName.match(o)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    function find_object(context, obj) {
        if (obj.____index !== undefined) {
            assert(context.objects[obj.____index] === obj);
            return obj.____index;
        }
        return -1;
    }

    function add_object_in_index(context, obj, serializing_data) {
        var id = context.index.length;
        obj.____index = id;
        context.index.push(serializing_data);
        context.objects.push(obj);
        return id;
    }

    function extract_object_classname(object) {
        var className = object.constructor.name;
        if (className) {
            return className;
        }
        /* in some old version of node className could be null */
        // istanbul ignore next
        if (true) {
            if (object instanceof Float32Array) { return "Float32Array"; }
            if (object instanceof Uint32Array) { return "Uint32Array"; }
            if (object instanceof Uint16Array) { return "Uint16Array"; }
            if (object instanceof Uint8Array) { return "Uint8Array"; }
            if (object instanceof Int32Array) { return "Int32Array"; }
            if (object instanceof Int16Array) { return "Int16Array"; }
            if (object instanceof Int8Array) { return "Int8Array"; }    
        }
    }

    function _serialize_xxx(context, options, object, construct, serialize) {
        // check if the object has already been serialized
        let id = find_object(context, object);
        if (id === -1) {
            const stuff = construct();
            id = add_object_in_index(context, object, stuff);
            serialize(context, object, stuff, options);
        }
        return id;
    }
    function _deserialize_xxx(context, object_id, contruct, deserialize) {
        assert(object_id);
        // check if this object has already been de-serialized
        if (context.cache[object_id] !== undefined) {
            return context.cache[object_id];
        }
        const newStuff = contruct();
        context.cache[object_id] = newStuff;
        const serializing_data = context.index[object_id];
        deserialize(context, newStuff, serializing_data);
        return newStuff;
    }

    function _serialize_map(context, options, object) {
        return _serialize_xxx(context, options, object,
            () => [],
            (context, object, mapJson, options) => {
                for (const [key, value] of object.entries()) {
                    mapJson.push([
                        _serialize(context, options, key),
                        _serialize(context, options, value),
                    ])
                }
            });
    }
    function _deserialize_map(context, object_id) {
        return _deserialize_xxx(context, object_id,
            () => new Map(),
            (context, newMap, serializing_data) => {
                for (const [key, value] of serializing_data) {
                    const k = deserialize_node_or_value(context, key);
                    const v = deserialize_node_or_value(context, value);
                    newMap.set(k, v);
                }
            })
    }
    function _serialize_set(context, options, object) {
        return _serialize_xxx(context, options, object,
            () => [],
            (context, object, setJson, options) => {
                for (const value of object.values()) {
                    setJson.push(_serialize(context, options, value))
                }
            });
    }
    function _deserialize_set(context, object_id) {
        return _deserialize_xxx(context, object_id,
            () => new Set(),
            (context, newSet, serializing_data) => {
                for (const value of serializing_data) {
                    const v = deserialize_node_or_value(context, value);
                    newSet.add(v)
                }
            })
    }

    function _serialize_basic_object(context, options, object) {
        const className = extract_object_classname(object);

        // istanbul ignore next
        if (className !== "Object" && !g_classInfos.hasOwnProperty(className)) {
            console.log(object);
            throw new Error("class " + className + " is not registered in class Factory - deserialization will not be possible");
        }
        return _serialize_xxx(context, options, object,
            () => ({ c: className }),
            (context, object, s, options) =>
                g_classInfos[className].serializeFunc(context, object, s, options));
    }
    function _deserialize_basic_object(context, object_id) {
        if (object_id === null) {
            return null;
        }
        // check if this object has already been de-serialized
        if (context.cache[object_id] !== undefined) {
            return context.cache[object_id];
        }
        var serializing_data = context.index[object_id];
        var cache_object = _deserialize_object(context, serializing_data, object_id);
        assert(context.cache[object_id] === cache_object);
        return cache_object;
    }
    function _serialize_object(context, options, serializingObject, object) {

        assert(context);
        assert(object !== undefined);
        if (object === null) {
            serializingObject.o = null;
            return;
        }
        const className = extract_object_classname(object);

        // j => json object to follow
        // d => date
        // a => array
        // o => class  { c: className d: data }
        // o => null
        // @ => already serialized object
        // s => Set
        // m => Map

        if (className === "Array") {
            serializingObject.a = object.map(_serialize.bind(null, context, options));
        } else if (object.constructor === Map) {
            serializingObject.m = _serialize_map(context, options, object);
        } else if (object.constructor === Set) {
            serializingObject.s = _serialize_set(context, options, object);
        } else if (className === "Date") {
            serializingObject.d = object.getTime();
        } else {
            serializingObject.o = _serialize_basic_object(context, options, object);
        }
    }

    function _serialize(context, options, object) {

        assert(context);
        // istanbul ignore next
        if (object === undefined) {
            return undefined;
        }

        var serializingObject = {};
        var _throw = function () {
          throw new Error("invalid typeof " + typeof object + " " + JSON.stringify(object, null, " "));
        }

        switch (typeof object) {
            case 'number':
            case 'boolean':
            case 'string':
                // basic type
                return object;
            case 'object':
                _serialize_object(context, options, serializingObject, object);
                break;
            default:
                if (options.errorHandler) {
                  options.errorHandler(context, options, object, _throw)
                } else {
                  _throw()
                }
        }
        return serializingObject;
    }

    /**
     *
     * @param object            {object} object to serialize
     * @param [options]         {object} optional options
     * @param [options.ignored] {string|regexp|Array<string|regexp>} pattern for field to not serialize
     * @return {string}
     */
    function serialize(object, options) {

        assert(object !== undefined, "serialize: expect a valid object to serialize ");

        var context = {
            index: [],
            objects: []
        };

        var obj = _serialize(context, options, object);

        // unset temporary ___index properties
        context.objects.forEach(function (e) {
            delete e.____index;
        });

        return JSON.stringify([context.index, obj]);// ,null," ");
    }


    function deserialize_node(context, node) {
        assert(context);
        // special treatment
        if (node === null || node === undefined) {
            return node;
        }

        if (node.hasOwnProperty("s")) {
            return _deserialize_set(context, node.s);
        } else if (node.hasOwnProperty("m")) {
            return _deserialize_map(context, node.m);
        } else if (node.hasOwnProperty("d")) {
            return new Date(node.d);
        } else if (node.hasOwnProperty("o")) {
            return _deserialize_basic_object(context, node.o);
        } else if (node.hasOwnProperty("a")) {
            // return _deserialize_object(node.o);
            return node.a.map(deserialize_node_or_value.bind(null, context));
        }
        // istanbul ignore next
        throw new Error("Unsupported deserialize_node " + JSON.stringify(node));
    }

    function _deserialize_object(context, object_definition, object_id) {

        assert(object_definition.c);

        var className = object_definition.c;
        var classInfo = g_classInfos[className];

        // istanbul ignore next
        if (!classInfo) {
            throw new Error(" Cannot find constructor to deserialize class of type " + className + ". use declarePersistable(Constructor)");
        }
        var constructor = classInfo.constructor;
        assert(isFunction(constructor));

        var obj = classInfo.deserializeFunc(context, object_id, object_definition);

        if (constructor && constructor.serialijseOptions) {


            // onDeserialize is called immediately after object has been created
            if (constructor.serialijseOptions.onDeserialize) {
                constructor.serialijseOptions.onDeserialize(obj);
            }
            // onPostDeserialize call is postponed after the main object has been fully de-serializednpm
            if (constructor.serialijseOptions.onPostDeserialize) {
                context.postDeserialiseActions.push(obj);
            }
        }
        return obj;
    }

    function deserialize(serializationString) {

        var data;
        if (typeof serializationString === 'string') {
            data = JSON.parse(serializationString);
        } else if (typeof serializationString === 'object') {
            data = serializationString;
        }
        // istanbul ignore next
        if (!(data instanceof Array)) {
            throw new Error("Invalid Serialization data");
        }
        // istanbul ignore next
        if (data.length !== 2) {
            throw new Error("Invalid Serialization data");
        }

        var rawObject = data[1];
        var context = {
            index: data[0],
            cache: [],
            postDeserialiseActions: []
        };

        var deserializedObject = deserialize_node_or_value(context, rawObject);

        context.postDeserialiseActions.forEach(function (o) {
            o.constructor.serialijseOptions.onPostDeserialize(o);
        });

        return deserializedObject;
    }

    exports.deserialize = deserialize;
    exports.serialize = serialize;
    exports.declarePersistable = declarePersistable;

    exports.serializeZ = function (obj, callback) {
        var zlib = require("zlib");
        var str = serialize(obj);
        zlib.deflate(str, function (err, buff) {
            // istanbul ignore next
            if (err) {
                return callback(err);
            }
            callback(null, buff);
        });
    };

    exports.deserializeZ = function (data, callback) {
        var zlib = require("zlib");
        zlib.inflate(data, function (err, buff) {
            // istanbul ignore next
            if (err) {
                return callback(err);
            }
            callback(null, deserialize(buff.toString()));
        });
    };


})(typeof exports === 'undefined' ? this['serialijse'] = {} : exports);