All files / server/utils/options parse.js

100% Statements 71/71
100% Branches 94/94
100% Functions 4/4
100% Lines 68/68
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180        35x 35x             35x     1734x 43x       1691x       4770x 12x 1x   11x 1x   10x       4768x     4768x 477759x     4768x           1689x     1689x       8936x       8936x     8026x 3x       8023x 2x       8021x 7680x 341x 2x       8019x 7863x 156x 1x       8018x 8012x         8018x         32072x 8018x 4021x               8020x 152x       8020x 504x 343x   504x 504x 168x         8020x 317x               8020x 1x         8019x 3x       8016x     2569x   3489x     2569x 6x           8010x 3x 3x 2x     8008x 4013x         1671x 8895x 8895x 9x       1671x    
// parse.js
// Reads an schema and retrieves the proper options from it
 
// Errors specifics to this submodule
const OptionsError = require('./errors');
const path = require('path');
 
 
// The main function.
// - arg: user options from the argument in the main server() function
// - env: the environment variables as read from env.js
// - parent: if it's a submodule, the global configuration
const parse = module.exports = async (schema, ...args) => {
 
  // For plugins, accept "false" to nuke a full plugin
  if (args.includes(false)) {
    return false;
  }
 
  // Clean them and put them into their names
  const [env = {}, arg = {}, parent = {}] = args.map((dirty = {}) => {
 
    // Accepts a single option instead of an object and it will be mapped to its
    // root value. Example: server(2000) === server({ port: 2000 })
    if (typeof dirty !== 'object') {
      if (!schema.__root) {
        throw new OptionsError('notobject');
      }
      if (typeof schema.__root !== 'string') {
        throw new OptionsError('rootnotstring');
      }
      dirty = { [schema.__root.toLowerCase()]: dirty };
    }
 
    // Clone them to remove the references
    let opts = {};
 
    // Loop and assign them with lowercase. Everything should be lowercase:
    for (const key in dirty) {
      opts[key.toLowerCase()] = dirty[key];
    }
 
    return opts;
  });
 
 
 
  // Fully parsed options will be stored here
  const options = {};
 
  // Loop each of the defined options
  for (let name in schema) {
 
    // RETRIEVAL
    // Make the definition local so it's easier to handle
    const def = schema[name];
    let value;
 
    // Skip the control variables such as '__root'
    if (/^__/.test(name)) continue;
 
    // Make sure we are dealing with a valid schema definition for this option
    if (typeof def !== 'object') {
      throw new OptionsError('noobjectdef', { name, type: typeof def });
    }
 
    // The user defined a function to find the actual value manually
    if (def.find) {
      value = await def.find({ arg, env, def, parent, schema });
    } else {
 
      // Use the user-passed option unles explictly told not to
      if (def.arg !== false) {
        def.arg = def.arg === true ? name : def.arg || name;
      } else if (arg[name] && env.node_env === 'test') {
        throw new OptionsError('noarg', { name });
      }
 
      // Use the environment variable unless explicitly told not to
      if (def.env !== false) {
        def.env = (def.env === true ? name : def.env || name).toLowerCase();
      } else if (env[name] && env.node_env === 'test') {
        throw new OptionsError('noenv', { name });
      }
 
      // Make sure to use the name if we are inheriting with true
      if (def.inherit !== false) {
        def.inherit = (def.inherit === true ? name : def.inherit || name);
      }
 
      // List of possibilities, from HIGHER preference to LOWER preference
      // Removes the empty one and gets the first one as it has HIGHER preference
      const possible = [
        env[def.env],
        arg[def.arg],
        parent[def.inherit],
        def.default
      ].filter(value => typeof value !== 'undefined');
      if (possible.length) {
        value = possible[0];
      }
    }
 
    // Extend the base object or user object with new values if these are not set
    // Extend only if it's an object
    // Note: some times it accepts object (to extend) or boolean, so it cannot
    // force to always be object
    if (def.extend && typeof value === 'object') {
      value = Object.assign({}, def.default || {}, value);
    }
 
    // Normalize the "public" folder or file
    if ((def.file || def.folder) && typeof value === 'string') {
      if (!path.isAbsolute(value)) {
        value = path.join(process.cwd(), value);
      }
      value = path.normalize(value);
      if (def.folder && value[value.length - 1] !== path.sep) {
        value = value + path.sep;
      }
    }
 
    // A final hook for the schema to call up on the value
    if (def.clean) {
      value = await def.clean(value, { arg, env, parent, def, schema });
    }
 
 
 
 
    // VALIDATION
    // Validate that it is set
    if (def.required && typeof value === 'undefined') {
      throw new OptionsError('required', { name });
 
      // TODO: check that the file and folder exist
    }
 
    if (def.enum && value && !def.enum.includes(value)) {
      throw new OptionsError('enum', { name, value, possible: def.enum });
    }
 
    // Validate the type (only if there's a value)
    if (def.type && value) {
 
      // Parse valid types into a simple array of strings: ['string', 'number']
      def.type = (def.type instanceof Array ? def.type : [def.type])
        // pulls up the name for primitives such as String, Number, etc
        .map(one => (one.name ? one.name : one).toLowerCase());
 
      // Make sure it is one of the valid types
      if (!def.type.includes(typeof value)) {
        throw new OptionsError('type', {
          name, expected: def.type, value
        });
      }
    }
 
    if (def.validate) {
      let ret = def.validate(value, def, options);
      if (ret instanceof Error) throw ret;
      if (!ret) throw new OptionsError('validate', { name, value });
    }
 
    if (typeof value !== 'undefined') {
      options[name] = value;
    }
  }
 
  // If the property 'options' exists handle it recursively
  for (let name in schema) {
    const def = schema[name];
    if (def.options) {
      options[name] = await parse(def.options, env, arg[name], options);
    }
  }
 
  return options;
};