bakerface/take-action

View on GitHub
index.js

Summary

Maintainability
B
5 hrs
Test Coverage
/**
 * Copyright (c) 2016 Chris Baker <mail.chris.baker@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

var ActionCreateError = exports.ActionCreateError = function (message) {
  Error.call(this);

  this.name = 'ActionCreateError';
  this.message = message;
};

var ActionValidateError = exports.ActionValidateError = function (errors) {
  Error.call(this);

  this.name = 'ActionValidateError';
  this.message = 'The action could not be validated';
  this.errors = errors;
};

function createTypeError(actual, expected) {
  return new ActionValidateError('Expected type to be "' + expected +
    '" but found "' + actual + '"');
}

function extend(target, values) {
  for (var key in values) {
    if (Object.prototype.hasOwnProperty.call(values, key)) {
      target[key] = values[key];
    }
  }

  return target;
}

function identity(x) {
  return x;
}

function compose(f, g) {
  return function (x) {
    return f(g(x));
  };
}

function defineGetter(target, name, get) {
  Object.defineProperty(target, name, { get: get });
}

function ActionTypes(validate) {
  this.validate = validate || identity;

  defineGetter(this, 'string', function () {
    return this.typeOf('string');
  });

  defineGetter(this, 'object', function () {
    return this.typeOf('object');
  });

  defineGetter(this, 'bool', function () {
    return this.typeOf('boolean');
  });

  defineGetter(this, 'number', function () {
    return this.typeOf('number');
  });

  defineGetter(this, 'func', function () {
    return this.typeOf('function');
  });

  defineGetter(this, 'date', function () {
    return this.optional(function (value) {
      var date = new Date(value);
      var time = date.getTime();

      if (isNaN(time)) {
        throw createTypeError(typeof value, 'date');
      }

      return date;
    });
  });

  defineGetter(this, 'array', function () {
    return this.optional(function (value) {
      if (Array.isArray(value)) {
        return value;
      }

      throw createTypeError(typeof value, 'array');
    });
  });

  defineGetter(this, 'email', function () {
    return this.string.optional(function (value) {
      if (/^[^@]+@[^@]+$/.test(value)) {
        return value;
      }

      throw new ActionValidateError('An email address must have a domain');
    });
  });

  defineGetter(this, 'isRequired', function () {
    return this.compose(function (value) {
      if (typeof value === 'undefined') {
        throw new ActionValidateError('Required');
      }

      return value;
    });
  });
}

ActionTypes.prototype.compose = function (validate) {
  return new ActionTypes(compose(validate, this.validate));
};

ActionTypes.prototype.optional = function (validate) {
  return this.compose(function (value) {
    if (typeof value !== 'undefined') {
      return validate(value);
    }
  });
};

ActionTypes.prototype.typeOf = function (expected) {
  return this.optional(function (value) {
    var actual = typeof value;

    if (actual !== expected) {
      throw createTypeError(actual, expected);
    }

    return value;
  });
};

function get(object, key) {
  var value = object[key];

  if (typeof value === 'function') {
    return value.bind(object);
  }

  return value;
}

ActionTypes.prototype.shape = function (shape) {
  return this.object.optional(function (value) {
    var sanitized = { };
    var errors = { };
    var isError = false;

    for (var key in shape) {
      if (Object.prototype.hasOwnProperty.call(shape, key)) {
        try {
          sanitized[key] = shape[key].validate(get(value, key));
        }
        catch (err) {
          if (err instanceof ActionValidateError) {
            errors[key] = err.errors;
          }
          else {
            errors[key] = err.message;
          }

          isError = true;
        }
      }
    }

    if (isError) {
      throw new ActionValidateError(errors);
    }

    return sanitized;
  });
};

exports.Types = new ActionTypes();

exports.create = function (action) {
  if (typeof action !== 'object') {
    throw new ActionCreateError('An action is required');
  }

  if (typeof action.perform !== 'function') {
    throw new ActionCreateError('An action perform function is required');
  }

  return function (jacks, props) {
    var shape = new ActionTypes().shape({
      jacks: new ActionTypes().shape(action.jackTypes || { }),
      props: new ActionTypes().shape(action.propTypes || { })
    });

    var j = (action.getDefaultJacks) ? action.getDefaultJacks() : { };
    var p = (action.getDefaultProps) ? action.getDefaultProps() : { };

    var sanitized = shape.validate({
      jacks: extend(j, jacks || { }),
      props: extend(p, props || { })
    });

    return action.perform(sanitized.jacks, sanitized.props);
  };
};

function bindActionToJacks(action, jacks) {
  return function (props) {
    return action(jacks, props);
  };
}

exports.bindActionsToJacks = function (actions, jacks) {
  var target = { };

  for (var key in actions) {
    if (Object.prototype.hasOwnProperty.call(actions, key)) {
      if (typeof actions[key] === 'function') {
        target[key] = bindActionToJacks(actions[key], jacks);
      }
      else {
        target[key] = actions[key];
      }
    }
  }

  return target;
};