wbyoung/maguey

View on GitHub
lib/types/predicate_rule.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict';

var _ = require('lodash');
var util = require('util');
var Class = require('corazon/class');
var LiteralString = require('./literal');

/**
 * @class PredicateRule
 * @classdesc
 *
 * Predicate rules are used by custom adapters to define the structure of
 * predicates with respect both to their format and use of arguments.
 */
var PredicateRule = Class.extend(/** @lends PredicateRule# */ {

  /**
   * Create a predicate rule.
   *
   * @protected
   * @constructor PredicateRule
   */
  init: function() {
    this._transforms = [];
  },

  /**
   * An object representing the format and argument combination that results
   * from the expansion of this rule.
   *
   * The expansion is passed to transformers and is ultimately the value that
   * is used as the result of the {@link PredicateRule#expand} process.
   *
   * Before being transformed/expanded, this object uses the format string as
   * defined by {@link PredicateRule#using} and the `args` that are given to
   * the {@link PredicateRule#expand} process. While `args` should be
   * considered an array of opaque values, the process will almost always start
   * with two arguments in this array, a _field_ and a _value_. Throughout the
   * `PredicateRule` documentation, when referring to the _value_ within
   * `args`, it refers to this object at index 1 within the array before it has
   * been further transformed.
   *
   * @typedef {Object} PredicateRule~Expansion
   * @property {String} format The value of the format string.
   * @property {Array} args The array of args .
   */

  /**
   * @callback PredicateRule~Transform
   * @param {PredicateRule~Expansion} expansion The value to transform.
   * @return {PredicateRule~Expansion} The updated value to use instead.
   */

  /**
   * Add a transform for this predicate rule. Multiple transforms can be added
   * and are processed in the sequence that they were added. Transforms will be
   * applied during {@link PredicateRule#expand}.
   *
   * @param {PredicateRule~Transform} transform The transform to add.
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  transform: function(transform) {
    this._transforms.push(transform);
    return this;
  },

  /**
   * Set the RegExp to use for this predicate rule.
   *
   * @param {RegExp} regex The regex.
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  match: function(regex) {
    this._regex = regex;
    return this;
  },

  /**
   * Set the format string for the predicate rule.
   *
   * @param {String} format The format.
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  using: function(format) {
    this._format = format;
    this._transforms.splice(0);
    return this;
  },

  /**
   * Test this rule to see if it should be used with a given predicate string.
   *
   * @param {String} predicate The predicate to test.
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  test: function(predicate) {
    return this._regex.test(predicate);
  },

  /**
   * Expand the predicate rule using a set of opaque arguments to craft both
   * the resulting format string and args.
   *
   * @param {Array} args An array of opaque values.
   * @return {PredicateRule~Expansion} The expanded format & args.
   * @see {@link PredicateRule~Expansion}
   */
  expand: function(args) {
    var result = { format: this._format, args: args };
    this._transforms.forEach(function(fn) {
      result = fn(result);
    });
    return result;
  },

  /**
   * Add a {@link PredicateRule~Transform} that will expand the
   * [value argument]{@link PredicateRule~Expansion}, which must be an array.
   * This allows format strings to easily specify multiple placeholders and
   * accept array values to fill those (i.e. `between`).
   *
   * @method
   * @public
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  expandsValue: function() {
    var fn = function(ex) { // jscs:ignore jsDoc
      ex.args = ex.args.slice(0);
      ex.args = ex.args.concat(ex.args.pop());
      return ex;
    };
    return this.transform(fn);
  },

  /**
   * Add a {@link PredicateRule~Transform} that will expand the format string
   * and the [value argument]{@link PredicateRule~Expansion}, which must be an
   * array. In addition to expanding the value argument, it will replace the
   * first occurrence of `%@` in the format string with the comma separated
   * `%s` strings to match the number of items in the _value_ array. This
   * allows format strings to expand based on value arguments (i.e. to support
   * `in` and similar predicates).
   *
   * @method
   * @public
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  expands: function() {
    var fn = function(ex) { // jscs:ignore jsDoc
      var args = ex.args;
      var length = args[1].length;
      var placeholders = _.times(length, function() { return '%s'; });
      ex.format = ex.format.replace('%@', placeholders.join(', '));
      return ex;
    };
    this.transform(fn);
    this.expandsValue();
    return this;
  },

  /**
   * Add a {@link PredicateRule~Transform} that will alter arguments based on
   * the provided wrapping functions. This will only alter the _value_
   * argument, the argument at index _1_ in the list of arguments.
   *
   * The given transformation functions will be applied to the value in the
   * order in which they are provided to this function.
   *
   * @method
   * @public
   * @param {...(Function|String)} fn The transformation to apply to the value
   * argument. If a string, the named function is looked up on the
   * {@link PredicateRule} class object.
   * @return {PredicateRule} The predicate rule to allow chaining.
   */
  value: function() {
    var fns = _.toArray(arguments).map(function(fn) {
      return _.isString(fn) ? PredicateRule[fn] : fn;
    });
    var composed = _.compose.apply(null, fns.reverse());

    var fn = function(ex) { // jscs:ignore jsDoc
      ex.args = ex.args.slice(0);
      ex.args[1] = composed(ex.args[1]);
      return ex;
    };
    return this.transform(fn);
  },
});


PredicateRule.reopenClass(/** @lends PredicateRule */ {

  /**
   * Translate an individual argument to a literal. This is intended to be used
   * with {@link PredicateRule#value}.
   *
   * @method PredicateRule.literal
   * @public
   * @param {String} value The value to transform.
   * @return {LiteralString} The literal string.
   */
  literal: LiteralString.create.bind(LiteralString),

  /**
   * Translate an individual boolean argument into words representing a null
   * check. This is intended to be used with {@link PredicateRule#value} and
   * {@link PredicateRule.literal}.
   *
   * @method PredicateRule.isNull
   * @public
   * @param {Boolean} value The value to translate.
   * @return {String} The string `NULL` or `NOT NULL`.
   */
  isNull: function(value) {
    return value ? 'NULL' : 'NOT NULL';
  },

  /**
   * Wrap an individual argument with the proper _like_ characters to support
   * _contains_. This is intended to be used with {@link PredicateRule#value}
   * and {@link PredicateRule.like}.
   *
   * @method PredicateRule.contains
   * @public
   * @param {String} value The value to wrap.
   * @return {String} The wrapped value.
   */
  contains: _.partial(util.format, '%%%s%%'),

  /**
   * Wrap an individual argument with the proper _like_ characters to support
   * _startsWith_. This is intended to be used with {@link PredicateRule#value}
   * and {@link PredicateRule.like}.
   *
   * @method PredicateRule.startsWith
   * @public
   * @param {String} value The value to wrap.
   * @return {String} The wrapped value.
   */
  startsWith: _.partial(util.format, '%s%%'),

  /**
   * Wrap an individual argument with the proper _like_ characters to support
   * _endsWIth_. This is intended to be used with {@link PredicateRule#value}
   * and {@link PredicateRule.like}.
   *
   * @method PredicateRule.endsWith
   * @public
   * @param {String} value The value to wrap.
   * @return {String} The wrapped value.
   */
  endsWith: _.partial(util.format, '%%%s'),

  /**
   * Escape an individual argument to support use with _like_. This is intended
   * to be used with {@link PredicateRule#value}.
   *
   * @method PredicateRule.like
   * @public
   * @param {String} value The value to escape.
   * @return {String} The escaped value.
   */
  like: function(value) {
    return value
      .replace('\\', '\\\\')
      .replace('%', '\\%')
      .replace('_', '\\_');
  },

  /**
   * Convert an individual argument that could be a `RegExp` into a string. This
   * is intended to be used with {@link PredicateRule#value}.
   *
   * @method PredicateRule.regex
   * @public
   * @param {String|RegExp} value The value to convert.
   * @return {String} The string representation.
   */
  regex: function(value) {
    if (!(value instanceof RegExp)) { return value; }
    return value
      .toString()
      .replace(/^\//, '')
      .replace(/\/[gimy]*$/, '');
  },
});

module.exports = PredicateRule.reopenClass({ __name__: 'PredicateRule' });