bakerface/ziploc

View on GitHub
index.js

Summary

Maintainability
B
6 hrs
Test Coverage
/**
 * Copyright (c) 2016 Christopher M. Baker
 *
 * 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 Ziploc = module.exports = function (contents, givens) {
  this.contents = contents || [];
  this.givens = givens || [];
};

Ziploc.prototype.add = function (content) {
  return new Ziploc(this.contents.concat(content), this.givens);
};

function isUsable(instance) {
  return function (name) {
    return typeof instance[name] === 'function' &&
      name.length > 3 &&
      name.slice(0, 3) === 'get';
  };
}

function toContent(instance) {
  return function (name) {
    var index = name.indexOf('From');
    var resolve = instance[name];

    if (index < 0) {
      return {
        type: name.slice(3),
        dependencies: [],
        resolve: resolve,
        instance: instance
      };
    }

    return {
      type: name.slice(3, index),
      dependencies: name.slice(index + 4).split('And'),
      resolve: resolve,
      instance: instance
    };
  };
}

function useContent(ziploc, content) {
  return ziploc.add(content);
}

function use(ziploc, instance) {
  return Object.getOwnPropertyNames(instance)
    .filter(isUsable(instance))
    .map(toContent(instance))
    .reduce(useContent, ziploc);
}

function flatten(args) {
  return [].concat.apply([], args);
}

Ziploc.prototype.use = function () {
  return flatten(arguments).reduce(use, this);
};

Ziploc.use = function () {
  return new Ziploc().use(flatten(arguments));
};

Ziploc.prototype.given = function (type, value) {
  return new Ziploc(this.contents.slice(), this.givens.concat({
    type: type,
    value: value
  }));
};

Ziploc.given = function (type, value) {
  return new Ziploc().given(type, value);
};

function series(tasks, done) {
  var values = [];

  function append(error, value) {
    if (error) {
      return done(error);
    }

    values.push(value);
    next();
  }

  function next() {
    if (tasks.length > 0) {
      return tasks.shift()(append);
    }

    done(null, values);
  }

  next();
}

function first(tasks, done) {
  var firstError;

  function _done(error, value) {
    if (!error) {
      return done(null, value);
    }

    if (!firstError) {
      firstError = error;
    }

    next();
  }

  function next() {
    var task = tasks.shift();

    if (task) {
      return task(_done);
    }

    done(firstError);
  }

  next();
}

function invoke(fn, instance, args, done) {
  var result;
  var called = false;

  function callback(error, value) {
    if (called) {
      return false;
    }

    called = true;

    setTimeout(function () {
      done(error, value);
    }, 0);
  }

  try {
    result = fn.apply(instance, args.concat(callback));
  }
  catch (error) {
    return callback(error);
  }

  if (result && typeof result.then === 'function') {
    return result.then(function (value) {
      callback(null, value);
    }, callback);
  }

  if (fn.length <= args.length) {
    return callback(null, result);
  }
}

function canResolveExplicit(type) {
  return function (content) {
    return content.type === type;
  };
}

function save(cache, type, done) {
  return function (error, value) {
    if (error) {
      return done(error, value);
    }

    cache[type] = value;
    return done(error, value);
  };
}

function createExplicitResolver(cache, contents) {
  return function (content) {
    return function (done) {
      resolveExplicit(cache, contents, content, done);
    };
  };
}

function resolveExplicit(cache, contents, content, done) {
  var tasks = content.dependencies
    .map(createTypeResolver(cache, contents));

  series(tasks, function (error, args) {
    if (error) {
      return done(error);
    }

    invoke(content.resolve, content.instance, args,
      save(cache, content.type, done));
  });
}

function isTemplated(type) {
  return type.indexOf('$') >= 0;
}

function rename(replacement) {
  return function (value) {
    return value.replace(/\$/g, replacement);
  };
}

function toExpression(type) {
  return '^' + rename('([A-Za-z0-9$]+)')(type) + '$';
}

function getMatchForTemplate(type, template) {
  return type.match(toExpression(template));
}

function isMatchForTemplate(type, template) {
  return getMatchForTemplate(type, template);
}

function isRecursive(type, content) {
  var recursive = content.dependencies
    .filter(function (dependency) {
      return isMatchForTemplate(dependency, content.type) &&
        isMatchForTemplate(type, dependency);
    });

  return recursive.length > 0;
}

function canResolveImplicit(type) {
  return function (content) {
    return isTemplated(content.type) &&
      isMatchForTemplate(type, content.type) &&
      !isRecursive(type, content);
  };
}

function createImplicitResolver(cache, contents, type) {
  return function (content) {
    return function (done) {
      resolveImplicit(cache, contents, type, content, done);
    };
  };
}

function Template(type) {
  this.type = type;
}

Template.prototype.toString = function () {
  return this.type;
};

Template.prototype.toCamelCase = function () {
  return this.type[0].toLowerCase() + this.type.slice(1);
};

Template.prototype.toUpperCase = function () {
  return this.type.toUpperCase();
};

Template.prototype.toLowerCase = function () {
  return this.type.toLowerCase();
};

Template.prototype.join = function (value) {
  return this.type[0] +
    this.type.slice(1).replace(/[A-Z]/g, function (c) {
      return value + c;
    });
};

Template.prototype.space = function () {
  return this.join(' ');
};

Template.prototype.toSnakeCase = function () {
  return this.join('_');
};

Template.prototype.toKebabCase = function () {
  return this.join('-');
};

function resolveImplicit(cache, contents, type, content, done) {
  var template = getMatchForTemplate(type, content.type)[1];

  var tasks = content.dependencies
    .map(rename(template))
    .map(createTypeResolver(cache, contents));

  series(tasks, function (error, args) {
    if (error) {
      return done(error);
    }

    args.unshift(new Template(template));

    invoke(content.resolve, content.instance, args,
      save(cache, type, done));
  });
}

function createTypeResolver(cache, contents) {
  return function (type) {
    return function (done) {
      resolveType(cache, contents, type, done);
    };
  };
}

function resolveType(cache, contents, type, done) {
  if (type in cache) {
    return done(null, cache[type]);
  }

  var explicit = contents.filter(canResolveExplicit(type))
    .map(createExplicitResolver(cache, contents));

  var implicit = contents.filter(canResolveImplicit(type))
    .map(createImplicitResolver(cache, contents, type));

  var tasks = explicit.concat(implicit);

  if (tasks.length === 0) {
    return done(new TypeError(type));
  }

  first(tasks, done);
}

function random(base, length) {
  var s = '';

  while (s.length < length) {
    s += Math.random().toString(base).slice(2);
  }

  return s.slice(0, length);
}

function uuid() {
  return 'ziploc:' + random(36, 25);
}

function getDependenciesForObject(object) {
  return Object.getOwnPropertyNames(object)
    .map(function (name) {
      return object[name];
    });
}

function assignResolvedProperty(object, dependencies, resolved) {
  return function (target, property) {
    var type = object[property];
    var index = dependencies.indexOf(type);
    var value = resolved[index];

    target[property] = value;
    return target;
  };
}

function resolveObjectFromDependencies(object, dependencies) {
  return function () {
    return Object.getOwnPropertyNames(object).reduce(
      assignResolvedProperty(object, dependencies, arguments), { });
  };
}

function reduceCreateCache(cache, given) {
  cache[given.type] = given.value;
  return cache;
}

function createCache(givens) {
  return givens.reduce(reduceCreateCache, { });
}

Ziploc.prototype.resolve = function (type, done) {
  if (typeof type === 'object') {
    var dependencies = getDependenciesForObject(type);

    var content = {
      type: uuid(),
      dependencies: dependencies,
      resolve: resolveObjectFromDependencies(type, dependencies)
    };

    return this.add(content).resolve(content.type, done);
  }

  resolveType(createCache(this.givens), this.contents, type, done);
};

Ziploc.prototype.express = function (request) {
  return new Express(this, request || 'Request');
};

function Express(ziploc, request) {
  this.ziploc = ziploc;
  this.request = request;
}

Express.prototype.status = function (code) {
  return new ExpressStatus(this.ziploc, this.request, code);
};

function ExpressStatus(ziploc, request, code) {
  this.ziploc = ziploc;
  this.request = request;
  this.code = code;
}

ExpressStatus.prototype.location = function (where) {
  return new ExpressStatusLocation(this.ziploc, this.request, this.code, where);
};

ExpressStatus.prototype.json = function (response) {
  var ziploc = this.ziploc;
  var request = this.request;
  var code = this.code;

  return function (req, res, next) {
    ziploc.given(request, req)
      .resolve(response, function (error, value) {
        if (error) {
          return next(error);
        }

        res.status(code).json(value);
      });
  };
};

function ExpressStatusLocation(ziploc, request, code, where) {
  this.ziploc = ziploc;
  this.request = request;
  this.code = code;
  this.where = where;
}

ExpressStatusLocation.prototype.json = function (response) {
  var ziploc = this.ziploc;
  var request = this.request;
  var code = this.code;

  var type = {
    location: this.where,
    json: response
  };

  return function (req, res, next) {
    ziploc
      .given(request, req)
      .resolve(type, function (error, value) {
        if (error) {
          return next(error);
        }

        res.status(code).location(value.location).json(value.json);
      });
  };
};