serialization.js

/*
 * node-qtdatastream
 * https://github.com/magne4000/node-qtdatastream
 *
 * Copyright (c) 2017 Joël Charles
 * Licensed under the MIT license.
 */

/** @module qtdatastream/serialization */

const { QUserType, QMap } = require('./types');

/**
 * A class using this decorator is serializable.
 * If usertype is not specified, objects from this class will be exported as QMap.
 * @static
 * @param {?string} usertype
 * @example
 * \@Serializable('Network::Server')
 * export class Server {
 *     \@serialize(QString, {in: 'HostIn', out: 'HostOut'))
 *     host;
 *
 *     \@serialize(QUInt, 'Port')
 *     port = 6667;
 *
 *     \@serialize(QUInt)
 *     sslVersion = 0;
 *
 *     constructor(args) {
 *         this.blob = true; // will not be serialized at export
 *         Object.assign(this, args);
 *     }
 * }
 *
 * @example
 * \@Serializable()
 * export class Server {
 *   x = 12;
 *
 *   _export() {
 *     return {
 *       'a': this.x
 *     };
 *   }
 * }
 */
function Serializable(usertype) {
  return function(aclass) {
    if (usertype) {
      Object.defineProperty(aclass, '__usertype', {
        enumerable: false,
        configurable: false,
        writable: false,
        value: usertype
      });
    }

    aclass.prototype.export = function() {
      const self = typeof this._export === 'function' ? this._export : () => this;
      let subject = this.__serialize ? this._mapping_out() : self();
      subject = deepMap(subject, prepare);
      return (this.constructor.__usertype ? QUserType.get(this.constructor.__usertype) : QMap).from(subject);
    };

    aclass.prototype._mapping_out = function() {
      const ret = {};
      const keys = Object.keys(this.__serialize);
      for (let key of keys) {
        Object.defineProperty(ret, key, {
          enumerable: true,
          configurable: true,
          writable: false,
          value: this.__serialize[key](this)
        });
      }
      return ret;
    };

    aclass.from = function(obj) {
      return new this(obj);
    };
  };
}

/**
 * A class attribute using this decorator is serializable
 * @static
 * @param {QClass} qclass
 * @param {(?string|{in: string, out: string})} serializekey
 * @example
 * \@Serializable()
 * export class Server {
 *     \@serialize(QString, {in: 'HostIn', out: 'HostOut'))
 *     host;
 *
 *     \@serialize(QUInt, 'Port')
 *     port = 6667;
 *
 *     \@serialize(QUInt)
 *     sslVersion = 0;
 *
 *     constructor(args) {
 *         Object.assign(this, args);
 *     }
 * }
 */
function serialize(qclass, serializekey = {}) {
  if (typeof serializekey === 'string') {
    serializekey = {
      in: serializekey,
      out: serializekey
    };
  }

  return function(aclass, key, descriptor) {
    if (!('set' in descriptor)) {
      descriptor.writable = true;
    }
    if (!aclass.hasOwnProperty('__serialize')) {
      Object.defineProperty(aclass, '__serialize', {
        enumerable: true,
        configurable: false,
        writable: false,
        value: {}
      });
    }
    // How to export
    Object.defineProperty(aclass.__serialize, serializekey.out || key, {
      enumerable: true,
      configurable: false,
      writable: false,
      value: context => qclass.from(context[key])
    });
    // How to import
    if (serializekey.in !== undefined && serializekey.in !== key) {
      Object.defineProperty(aclass, serializekey.in, {
        enumerable: false,
        set: function(value) {
          this[key] = value;
        },
        get: function() {
          return this[key];
        }
      });
    }
    return descriptor;
  };
}

function prepare(obj) {
  return (obj !== undefined && obj !== null && typeof obj.export === 'function') ? obj.export() : obj;
}

function mapObject(obj, fn) {
  const keys = Object.keys(obj);
  for (let key of keys) {
    const descriptor = Object.getOwnPropertyDescriptor(obj, key);
    Object.assign(descriptor, {
      value: fn(obj[key])
    });
    Object.defineProperty(obj, key, descriptor);
  }
  return obj;
}

function deepMap(obj, fn) {
  const deepMapper = val => (val !== null && typeof val === 'object') ? deepMap(val, fn) : fn(val);
  if (Array.isArray(obj)) {
    return obj.map(deepMapper);
  }
  if (obj !== null && typeof obj === 'object') {
    return mapObject(obj, deepMapper);
  }
  return obj;
}

module.exports = {
  Serializable,
  serialize
};