src/createResolver.js
import difference from 'lodash/array/difference';
import { merge, omit } from 'lodash/object';
import lang from 'lodash/lang';
import sortBy from 'lodash/collection/sortBy';
export default function createResolver() {
var state = {
defined: {},
defaults: {},
required: {},
resolved: {},
normalizers: {},
allowedValues: {},
allowedTypes: {},
lazy: {},
calling: {},
locked: false
};
var clone = {locked: false};
function setDefault(option, value) {
if (state.locked) {
throw new Error('Default values cannot be set from a lazy option or normalizer.');
}
if (!state.defined.hasOwnProperty(option)
|| null === state.defined[option]
|| state.resolved.hasOwnProperty(option)) {
state.resolved[option] = value;
}
state.defaults[option] = value;
state.defined[option] = true;
return this;
}
function setDefaults(defaults) {
for (const option of Object.keys(defaults)) {
setDefault(option, defaults[option]);
}
return this;
}
function hasDefault(option) {
return state.defaults.hasOwnProperty(option);
}
function setRequired(optionNames) {
if (state.locked) {
throw new Error('Options cannot be made required from a lazy option or normalizer.');
}
if (!Array.isArray(optionNames)) {
optionNames = [optionNames];
}
for (const option of optionNames) {
state.defined[option] = true;
state.required[option] = true;
}
return this;
}
function isRequired(option) {
return (state.required.hasOwnProperty(option)
&& null !== state.required[option]);
}
function getRequiredOptions() {
return Object.keys(state.required);
}
function isMissing(option) {
return (isRequired(option) && !hasDefault(option));
}
function getMissingOptions() {
return difference(Object.keys(state.required), Object.keys(state.defaults));
}
function setDefined(optionNames) {
if (state.locked) {
throw new Error('Options cannot be defined from a lazy option or normalizer.');
}
if (!Array.isArray(optionNames)) {
optionNames = [optionNames];
}
for (const option of optionNames) {
state.defined[option] = true;
}
return this;
}
function isDefined(option) {
return (state.defined.hasOwnProperty(option) && null !== state.defined[option]);
}
function getDefinedOptions() {
return Object.keys(state.defined);
}
function setNormalizer(option, normalizer) {
if (state.locked) {
throw new Error('Normalizers cannot be set from a lazy option or normalizer.');
}
if (!isDefined(option)) {
const definedOptions = Object.keys(state.defined).join('", "');
throw new Error(`The option "${option}" does not exist. Defined options are : "${definedOptions}"`);
}
state.normalizers[option] = normalizer;
state.resolved = omit(state.resolved, option);
return this;
}
function setAllowedValues(option, values) {
if (state.locked) {
throw new Error('Allowed values cannot be set from a lazy option or normalizer.');
}
if (!isDefined(option)) {
const definedOptions = Object.keys(state.defined).join('", "');
throw new Error(`The option "${option}" does not exist. Defined options are : "${definedOptions}"`);
}
state.allowedValues[option] = Array.isArray(values) ? values : [values];
state.resolved = omit(state.resolved, option);
return this;
}
function addAllowedValues(option, values) {
if (state.locked) {
throw new Error('Allowed values cannot be set from a lazy option or normalizer.');
}
if (!isDefined(option)) {
const definedOptions = Object.keys(state.defined).join('", "');
throw new Error(`The option "${option}" does not exist. Defined options are : "${definedOptions}"`);
}
if (!Array.isArray(values)) {
values = [values];
}
if (!state.allowedValues.hasOwnProperty(option) || null === state.allowedValues[option]) {
state.allowedValues[option] = values;
} else {
state.allowedValues[option] = [...state.allowedValues[option], ...values];
}
state.resolved = omit(state.resolved, option);
return this;
}
function setAllowedTypes(option, types) {
if (state.locked) {
throw new Error('Allowed types cannot be set from a lazy option or normalizer.');
}
if (!isDefined(option)) {
const definedOptions = Object.keys(state.defined).join('", "');
throw new Error(`The option "${option}" does not exist. Defined options are : "${definedOptions}"`);
}
state.allowedTypes[option] = Array.isArray(types) ? types : [types];
state.resolved = omit(state.resolved, option);
return this;
}
function addAllowedTypes(option, types) {
if (state.locked) {
throw new Error('Allowed types cannot be set from a lazy option or normalizer.');
}
if (!isDefined(option)) {
const definedOptions = Object.keys(state.defined).join('", "');
throw new Error(`The option "${option}" does not exist. Defined options are : "${definedOptions}"`);
}
if (!Array.isArray(types)) {
types = [types];
}
if (!state.allowedTypes.hasOwnProperty(option) || null === state.allowedTypes[option]) {
state.allowedTypes[option] = types;
} else {
state.allowedTypes[option] = [...state.allowedTypes[option], ...types];
}
state.resolved = omit(state.resolved, option);
return this;
}
function remove(optionNames) {
if (state.locked) {
throw new Error('Options cannot be removed from a lazy option or normalizer.');
}
state.defined = omit(state.defined, optionNames);
state.defaults = omit(state.defaults, optionNames);
state.required = omit(state.required, optionNames);
state.resolved = omit(state.resolved, optionNames);
state.lazy = omit(state.lazy, optionNames);
state.normalizers = omit(state.normalizers, optionNames);
state.allowedValues = omit(state.allowedValues, optionNames);
state.allowedTypes = omit(state.allowedTypes, optionNames);
return this;
}
function clear() {
if (state.locked) {
throw new Error('Options cannot be cleared from a lazy option or normalizer.');
}
state.defined = {};
state.defaults = {};
state.required = {};
state.resolved = {};
state.lazy = {};
state.normalizers = {};
state.allowedValues = {};
state.allowedTypes = {};
state.calling = {};
return this;
}
function resolve(options = {}) {
return new Promise((resolve, reject) => {
if (state.locked) {
const err = new Error('Options cannot be state.resolved from a lazy option or normalizer.');
return reject(err);
}
clone = lang.clone(state, true);
const definedDiff = difference(Object.keys(options), Object.keys(clone.defined));
if (definedDiff.length) {
const definedKeys = sortBy(Object.keys(clone.defined)).join('", "');
const diffKeys = sortBy(definedDiff).join('", "');
const err = `The option(s) "${diffKeys}" do not exist. Defined options are: "${definedKeys}"`;
return reject(err);
}
clone.defaults = merge(clone.defaults, options);
clone.resolved = omit(clone.resolved, Object.keys(options));
clone.lazy = omit(clone.lazy, options);
const requiredDiff = difference(Object.keys(clone.required), Object.keys(clone.defaults));
if (requiredDiff.length) {
const diffKeys = sortBy(requiredDiff).join('", "');
const err = `The required options "${diffKeys}" are missing`;
return reject(err);
}
clone.locked = true;
for (const option of Object.keys(clone.defaults)) {
get(option);
}
const resolved = lang.clone(clone.resolved, true);
clone = {locked: false};
resolve(resolved);
});
}
function get(option) {
if (!clone.locked) {
throw new Error('get is only supported within closures of lazy options and normalizers.');
}
if (clone.resolved.hasOwnProperty(option)) {
return clone.resolved[option];
}
if (!clone.defaults.hasOwnProperty(option)) {
if (!clone.defined.hasOwnProperty(option) || null === clone.defined[option]) {
const definedOptions = Object.keys(clone.defined).join('", "');
throw new Error(`The option "${option}" does not exist. Defined options are : "${definedOptions}"`);
}
throw new Error(`The optional option "${option}" has no value set. You should make sure it is set with "isset" before reading it.`);
}
let value = clone.defaults[option];
// @todo : process lazy option
if (clone.allowedTypes.hasOwnProperty(option)
&& null !== clone.allowedTypes[option]) {
let valid = false;
for (const allowedType of clone.allowedTypes[option]) {
var functionName = 'is' + allowedType.charAt(0).toUpperCase() + allowedType.substr(1).toLowerCase();
if (lang.hasOwnProperty(functionName)) {
if (lang[functionName](value)) {
valid = true;
break;
}
continue;
}
if (typeof value === allowedType) {
valid = true;
break;
}
}
if (!valid) {
// @todo add better log error
throw new Error(`Invalid type for option "${option}".`);
}
}
if (clone.allowedValues.hasOwnProperty(option)
&& null !== clone.allowedValues[option]) {
let success = false;
let printableAllowedValues = [];
for (const allowedValue of clone.allowedValues[option]) {
if (lang.isFunction(allowedValue)) {
if (allowedValue(value)) {
success = true;
break;
}
continue;
} else if (value === allowedValue) {
success = true;
break;
}
printableAllowedValues.push(allowedValue);
}
if (!success) {
let message = `The option "${option}" is invalid.`;
if (printableAllowedValues.length) {
message += ' Accepted values are : ' + printableAllowedValues.join(', ');
}
throw new Error(message);
}
}
if (clone.normalizers.hasOwnProperty(option)
&& null !== clone.normalizers[option]) {
if (clone.calling.hasOwnProperty(option) && null !== clone.calling[option]) {
const callingKeys = Object.keys(clone.calling).join('", "');
throw new Error(`The options "${callingKeys}" have a cyclic dependency`);
}
let normalizer = clone.normalizers[option];
clone.calling[option] = true;
try {
value = normalizer(value);
} finally {
clone.calling = omit(clone.calling, option);
}
}
clone.resolved[option] = value;
return value;
}
return {
setDefault,
setDefaults,
hasDefault,
setRequired,
isRequired,
getRequiredOptions,
isMissing,
getMissingOptions,
setDefined,
isDefined,
getDefinedOptions,
setNormalizer,
setAllowedValues,
addAllowedValues,
setAllowedTypes,
addAllowedTypes,
remove,
clear,
resolve,
get
}
}