core.js

const ConfigObjectModel = require('./model/com.js')

const ParserInterface = require('./performer/parser.js')
const GetterInterface = require('./performer/getter.js')
const RulerInterface = require('./performer/ruler.js')

const cvtError = require('./error.js')
const CUSTOMISE_FAILED = cvtError.CUSTOMISE_FAILED


/**
 * Cutomize Blueconfig before make a ConfigObjectModel (= COM, Like DOM but not for Document, for Config)
 *
 * @example
 * const Blueconfig = require('blueconfig/lib/core.js')
 * const blueconfig = new Blueconfig()
 * console.log(blueconfig.getGettersOrder()) // === [value', 'force']
 *
 * @class
 */
const BlueconfigCore = function() {
  this.initPerformer()
}

module.exports = BlueconfigCore


/**
 * Create instance of worker (Parser, Getter, Ruler)
 *
 * @return   {this}
 */
BlueconfigCore.prototype.initPerformer = function() {
  this.Parser = new ParserInterface()
  this.Getter = new GetterInterface()
  this.Ruler = new RulerInterface()

  return this
}


/**
 * Gets array with getter name in the current order of priority
 *
 * @example
 * const Blueconfig = require('blueconfig/lib/core.js')
 * const blueconfig = new Blueconfig()
 * blueconfig.init(rawSchema, options)
 *
 * @param   {string|object}   rawSchema    Schema object or a path to a schema JSON file
 * @param   {object}   [options]    Options. [See ConfigObjectModel constructor doc](./ConfigObjectModel.html)
 *
 * @return    {string[]}    Returns current getter order
 */
BlueconfigCore.prototype.init = function(rawSchema, options) {
  return new ConfigObjectModel(rawSchema, options, {
    Parser: this.Parser,
    Getter: this.Getter,
    Ruler: this.Ruler
  })
}


/**
 * Gets array with getter name in the current order of priority
 *
 * @example
 * blueconfig.getGettersOrder() // ['default', 'value', 'env', 'arg', 'force']
 *
 * @return    {string[]}    Returns current getter order
 */
BlueconfigCore.prototype.getGettersOrder = function() {
  return [...this.Getter.storage.order]
}


/**
 * Sorts getter priority, this function uses ascending order. It is recommanded to uses this function before init COM (with `blueconfig()`)
 * or you should call `<COM>.refreshGetters()`.
 *
 * @see ConfigObjectModel.refreshGetters
 *
 * @example
 * blueconfig.getGettersOrder()
 * // ['default', 'value', 'env', 'arg', 'force']
 *
 * // two ways to do:
 * blueconfig.sortGetters(['default', 'value', 'arg', 'env', 'force'])
 * blueconfig.sortGetters(['default', 'value', 'arg', 'env']) // force is optional and must be the last one
 *
 * blueconfig.getGettersOrder()
 * // ['default', 'value', 'arg', 'env', 'force']
 *
 * @param    {string[]}    newOrder       Value of the property to validate
 *
 * @return   {this}
 */
BlueconfigCore.prototype.sortGetters = function(newOrder) {
  const sortFilter = this.Getter.sortGetters(this.Getter.storage.order, newOrder)

  this.Getter.storage.order.sort(sortFilter)

  return this
}


/**
 * Adds a new custom getter. Getter function get value depending of the property name. In schema, the property name is a keyname of the schema.
 *
 * @example
 * convict.addGetter('aws', function(aws, schema, stopPropagation) {
 *   return aws.get(aws)
 * }, false, true)
 *
 * @param {string|object}       name                      String for Getter name, `Object/Object[]` or which contains arguments:
 * @param    {string}           name.name             Getter name
 * @param    {function}         name.getter               *See below*
 * @param    {boolean}          [name.usedOnlyOnce=false] *See below*
 * @param    {boolean}          [name.rewrite=false]      *See below*
 * @param    {ConfigObjectModel.getterCallback}   getter  Getter function get external value depending of the name name.
 * @param    {boolean}          [usedOnlyOnce=false]      `false` by default. If true, The value can't be reused by another `keyname=value`
 * @param    {boolean}          [rewrite=false]           Allow rewrite an existant format
 *
 * @return   {this}
 */
BlueconfigCore.prototype.addGetter = function(name, getter, usedOnlyOnce, rewrite) {
  if (typeof name === 'object') {
    getter = name.getter
    usedOnlyOnce = name.usedOnlyOnce
    rewrite = name.rewrite
    name = name.name || name.property
  }
  this.Getter.add(name, getter, usedOnlyOnce, rewrite)

  return this
}


/**
 * Adds several getters
 *
 * @example
 * // Example with the default env & arg getters:
 * Blueconfig.addGetters({
 *   env: {
 *     getter: (value, schema) => schema._cvtCoerce(this.getEnv()[value])
 *   },
 *   arg: {
 *     getter: function(value, schema, stopPropagation) {
 *       const argv = parseArgs(this.getArgs(), { configuration: { 'dot-notation': false } })
 *       return schema._cvtCoerce(argv[value])
 *     },
 *     usedOnlyOnce: true
 *   }
 * })
 *
 * @param {object|object[]}    getters                      Object containing list of Getters/Object
 * @param {object}    getters.{name}                        `name` is the getter name
 * @param {function}  getters.{name}.getter                 *See Blueconfig.addGetter*
 * @param {boolean}   [getters.{name}.usedOnlyOnce=false]   *See Blueconfig.addGetter*
 * @param {boolean}   [getters.{name}.rewrite=false]        *See Blueconfig.addGetter*
 *
 * @return   {this}
 */
BlueconfigCore.prototype.addGetters = function(getters) {
  if (Array.isArray(getters)) {
    getters.forEach((child) => {
      this.addGetter(child)
    })

    return this
  }
  Object.keys(getters).forEach((name) => {
    const child = getters[name]
    this.addGetter(name, child.getter, child.usedOnlyOnce, child.rewrite)
  })

  return this
}


/**
 * Adds a new custom format. Validate function and coerce function will be used to validate COM property with `format` type.
 *
 * @example
 * Blueconfig.addFormat({
 *   name: 'int',
 *   coerce: (value) => (typeof value !== 'undefined') ? parseInt(value, 10) : value,
 *   validate: function(value) {
 *     if (Number.isInteger(value)) {
 *       throw new Error('must be an integer')
 *     }
 *   }
 * })
 *
 *
 * @param {string|object}                name              String for Format name, `Object/Object[]` or which contains arguments:
 * @param    {string}                    name.name         Format name
 * @param    {function}                  name.validate     *See below*
 * @param    {function}                  name.coerce       *See below*
 * @param    {boolean}               [name.rewrite=false]  *See below*
 * @param {SchemaNode.validateCallback}  validate          Validate function, should throw if the value is wrong `Error` or [`LISTOFERRORS` (see example)](./ZCUSTOMERROR.LISTOFERRORS.html)
 * @param {SchemaNode.coerce}            coerce            Coerce function to convert a value to a specified function (can be omitted)
 * @param    {boolean}                   [rewrite=false]   Allow rewrite an existant format
 *
 * @return   {this}
 */
BlueconfigCore.prototype.addFormat = function(name, validate, coerce, rewrite) {
  if (typeof name === 'object') {
    validate = name.validate
    coerce = name.coerce
    rewrite = name.rewrite
    name = name.name
  }
  this.Ruler.add(name, validate, coerce, rewrite)

  return this
}


/**
 * Adds several formats
 *
 * @example
 * // Add several formats with: [Object, Object, Object, Object]
 * const vFormat = require('blueconfig-format-with-validator')
 * blueconfig.addFormats({ // add: email, ipaddress, url, token
 *   email: vFormat.email,
 *   ipaddress: vFormat.ipaddress,
 *   url: vFormat.url,
 *   token: {
 *     validate: function(value) {
 *       if (!isToken(value)) {
 *         throw new Error(':(')
 *       }
 *     }
 *   }
 * })
 *
 * @see Blueconfig.addFormat
 *
 * @param {object|object[]}    formats               Object containing list of Object
 * @param {object}    formats.{name}                 {name} in `formats.{name}` is the format name
 * @param {function}  formats.{name}.validate        *See Blueconfig.addFormat*
 * @param {function}  formats.{name}.coerce          *See Blueconfig.addFormat*
 * @param {boolean}   [formats.{name}.rewrite=false] *See Blueconfig.addFormat*
 *
 * @return   {this}
 */
BlueconfigCore.prototype.addFormats = function(formats) {
  if (Array.isArray(formats)) {
    formats.forEach((child) => {
      this.addFormat(child)
    })

    return this
  }
  Object.keys(formats).forEach((name) => {
    this.addFormat(name, formats[name].validate, formats[name].coerce, formats[name].rewrite)
  })

  return this
}


/**
 * Adds new custom file parsers. JSON.parse will be used by default for unknown extension (default extension -> `*` => JSON).
 *
 * Blueconfig is able to parse files with custom file types during `merge`. For this specify the
 * corresponding parsers with the associated file extensions.
 *
 * If no supported extension is detected, `merge` will fallback to using the default json parser `JSON.parse`.
 *
 * @example
 * blueconfig.addParser([
 *  { extension: ['yml', 'yaml'], parse: yaml.safeLoad },
 *  { extension: ['yaml', 'yml'], parse: require('yaml').safeLoad },
 *  // will allow comment in json file
 *  { extension: 'json', parse: require('json5').parse } // replace `JSON` by `json5`
 * ])
 * config.merge('config.yml')
 *
 * @param    {object[]}    parsers              Parser
 * @param    {string}      parsers.extension    Parser extension
 * @param    {function}    parsers.parse        Parser function
 *
 * @return   {this}
 */
BlueconfigCore.prototype.addParser = function(parsers) {
  if (!Array.isArray(parsers)) parsers = [parsers]

  parsers.forEach((parser) => {
    if (!parser) throw new CUSTOMISE_FAILED('Invalid parser')
    if (!parser.extension) throw new CUSTOMISE_FAILED('Missing parser.extension')
    if (!parser.parse) throw new CUSTOMISE_FAILED('Missing parser.parse function')

    this.Parser.add(parser.extension, parser.parse)
  })

  return this
}