dataclasses/predicates.js

const merge = require('ramda/src/merge');
const mergeAll = require('ramda/src/mergeAll');
const $_type = require('./$_type');

module.exports = ({ invariants, errors }) => {
  const { Target, Operator, LogicalType } = require('./columns');
  /**
   * Abstract Predicate type, a Predicate is the union type of CompoundPredicate | ComparisonPredicate
   * @typedef {object} Predicate
   * @memberof dataclasses
   */

  /**
   * Abstract Predicate type, a Predicate is the union type of CompoundPredicate | ComparisonPredicate
   * @param {Function} type - Predicate subtype function constructor
   * @return {dataclasses.Predicate} predicate
   * @memberof dataclasses
   */
  function Predicate(type) {
    return invariants
      .PredicateTypeMustBeValid(type.$name, Predicate.Types)
      .then(() =>
        merge($_type(type.$name), {
          /**
           * $canBeRemoved specify if the predicate can be removed or not from the Predicates tree
           * @type {Boolean}
           *  @memberof Predicate
           */
          $canBeRemoved: true,
        })
      );
  }

  /**
   * [description]
   * @param  {dataclasses.Predicate} predicate predicate
   * @return {Object} serializable object
   */
  Predicate.toJSON = function(predicate) {
    if (ComparisonPredicate.is(predicate))
      return ComparisonPredicate.toJSON(predicate);

    return CompoundPredicate.toJSON(predicate);
  };

  /**
   * @param  {object} json json
   * @param  {object} internalAPI internalAPI
   * @return {Promise<Predicate, errors<*>>} Promise
   */
  Predicate.fromJSON = function(json, internalAPI) {
    if (ComparisonPredicate.isFromJSON(json))
      return ComparisonPredicate.fromJSON(json, internalAPI);

    if (CompoundPredicate.isFromJSON(json))
      return CompoundPredicate.fromJSON(json, internalAPI);

    return Promise.reject(new errors.UnknownJSONData());
  };

  Predicate.Types = {
    ComparisonPredicate: 'ComparisonPredicate',
    CompoundPredicate: 'CompoundPredicate',
  };

  /**
   * Abstract Predicate type, a Predicate is the union type of CompoundPredicate | ComparisonPredicate
   * @typedef {object} ComparisonPredicate
   * @param {string} target - unique id for this target
   * @param {string} operator - label that will be displayed for this target
   * @param {string} argument - the type_id name this target has
   * @memberof dataclasses
   */

  /**
   * A specialized predicate that you use to compare expressions.
   * @param  {dataclasses.Target} target target
   * @param  {dataclasses.Operator} operator operator
   * @param  {*} argument argument
   * @return {Promise<dataclasses.ComparisonPredicate>} yield a ComparisonPredicate or a rejected promise
   * @memberof dataclasses
   */
  function ComparisonPredicate(target, operator, argument = null) {
    return Predicate(ComparisonPredicate).then(predicate =>
      merge(predicate, {
        target,
        operator,
        argument,
      })
    );
  }

  // by pass var. mangling from minify
  ComparisonPredicate.$name = Predicate.Types.ComparisonPredicate;

  /**
   * @param  {ComparisonPredicate} predicate predicate
   * @return {Object} JSON serializable object
   */
  ComparisonPredicate.toJSON = function(predicate) {
    return mergeAll([
      Target.toJSON(predicate.target),
      Operator.toJSON(predicate.operator),
      {
        argument: predicate.argument,
      },
    ]);
  };

  /**
   * @param  {object} json json
   * @param  {object} internalAPI internalAPI
   * @return {Promise<Predicate, errors<*>>} Promise
   */
  ComparisonPredicate.fromJSON = function(json, internalAPI) {
    return Promise.all([
      internalAPI.getTargetById(json.target_id),
      internalAPI.getOperatorById(json.operator_id),
    ]).then(([target, operator]) =>
      ComparisonPredicate(target, operator, json.argument)
    );
  };

  /**
   * Yield true if `predicate` is a ComparisonPredicate
   * @param  {dataclasses.Predicate}  predicate {@link dataclasses.Predicate}
   * @return {Boolean} true if `predicate` is a ComparisonPredicate
   * @memberof dataclasses
   */
  ComparisonPredicate.is = predicate => {
    return (
      predicate && predicate.$_type === Predicate.Types.ComparisonPredicate
    );
  };

  /**
   * Yield true if `json` seems to be a ComparisonPredicate
   * @param  {object} json json
   * @private
   * @return {Boolean} true if json seems to be a ComparisonPredicate
   * @memberof dataclasses
   */
  ComparisonPredicate.isFromJSON = json => json && json.target_id;

  /**
   * A specialized predicate that evaluates logical combinations of other predicates.
   * @param {dataclasses.LogicalType} logic The predicate logic
   * @param {Array<dataclasses.Predicate>} predicates predicates predicates
   * @return {Promise<dataclasses.CompoundPredicate>} yield a {@link dataclasses.CompoundPredicate} or a {@link errors.CompoundPredicateMustHaveAtLeastOneSubPredicate} rejected promise
   * @memberof dataclasses
   */
  function CompoundPredicate(logic, predicates) {
    return invariants
      .CompoundPredicateMustHaveAtLeastOneSubPredicate(
        predicates,
        CompoundPredicate
      )
      .then(() => Predicate(CompoundPredicate))
      .then(predicate =>
        merge(predicate, {
          logic,
          predicates,
        })
      );
  }

  // by pass var. mangling from minify
  CompoundPredicate.$name = Predicate.Types.CompoundPredicate;

  /**
   * @param  {CompoundPredicate} predicate predicate
   * @return {Object} JSON serializable object
   */
  CompoundPredicate.toJSON = function(predicate) {
    return mergeAll([
      LogicalType.toJSON(predicate.logic),
      { predicates: predicate.predicates.map(Predicate.toJSON) },
    ]);
  };

  /**
   * @param  {CompoundPredicate} predicate predicate
   * @param  {object} internalAPI ui-predicate-core internal api object
   * @return {Promise<CompoundPredicate, errors<*>>} Promise
   */
  CompoundPredicate.fromJSON = function(predicate, internalAPI) {
    return invariants
      .CompoundPredicateMustHaveAtLeastOneSubPredicate(
        predicate.predicates,
        CompoundPredicate
      )
      .then(() => internalAPI.getLogicalTypeById(predicate.logicalType_id))
      .then(logicalType =>
        Promise.all(
          predicate.predicates.map(predicate =>
            Predicate.fromJSON(predicate, internalAPI)
          )
        ).then(predicates => CompoundPredicate(logicalType, predicates))
      );
  };

  /**
   * Reduce through the predicates tree
   * @param       {dataclasses.CompoundPredicate} compoundPredicate starter node
   * @param       {function} f accumulation function, f(acc, predicate, parents)
   * @param       {T} acc               accumulator
   * @param       {Array}  [parents=[]]      path to the node, array of parents
   * @return      {T} yield the accumulator
   * @memberof dataclasses
   */
  CompoundPredicate.reduce = function(compoundPredicate, f, acc, parents = []) {
    const accumulator = f(acc, compoundPredicate, parents);
    return compoundPredicate.predicates.reduce((_acc, predicate, i) => {
      const _parents = parents.concat([compoundPredicate, [predicate, i]]);
      return CompoundPredicate.is(predicate)
        ? CompoundPredicate.reduce(predicate, f, _acc, _parents)
        : f(_acc, predicate, _parents);
    }, accumulator);
  };

  /**
   * Walk through the predicates tree
   * @param       {dataclasses.CompoundPredicate} compoundPredicate starter node
   * @param       {Function} f(predicate) iterator function
   * @return {undefined}
   * @memberof dataclasses
   */
  CompoundPredicate.forEach = (compoundPredicate, f) => {
    CompoundPredicate.reduce(
      compoundPredicate,
      (_, predicate) => {
        f(predicate);
      },
      null
    );
  };

  /**
   * Yield true if `predicate` is a CompoundPredicate
   * @param  {dataclasses.Predicate}  predicate predicate
   * @return {Boolean} true if `predicate` is a CompoundPredicate
   * @memberof dataclasses
   */
  CompoundPredicate.is = predicate =>
    predicate && predicate.$_type === Predicate.Types.CompoundPredicate;

  /**
   * Yield true if `json` seems to be a CompoundPredicate
   * @param  {object}  json json
   * @private
   * @return {Boolean} true if json seems to be a CompoundPredicate json
   * @memberof dataclasses
   */
  CompoundPredicate.isFromJSON = json => json && json.logicalType_id;

  return {
    Predicate,
    ComparisonPredicate,
    CompoundPredicate,
  };
};