0.0.5 • Published 7 years ago

config-one v0.0.5

Weekly downloads
3
License
PDDL-1.0
Repository
github
Last release
7 years ago

config-one

Build Status Join the chat at https://gitter.im/Klortho/config-one

A zero configuration (but highly configurable) library for managing app configuration data. This module uses a simple yet sophisticated algorithm, based on functional programming principles, to provide a robust and
flexible mechanism for managing and merging configuration data trees.

Alpha version

This library is still in a very early stage, and the API is likely to change.

Also, currently this requires Node.js version 6. I'll fix the build process so that the distribution version can be used with earlier versions, but that is not yet done.

Features

  • Flexible, robust overrides. You can overlay new configation sources at any time.
  • Computed configuration settings. Any setting can compute its value from any other setting, using "recipes" (which are just JavaScript functions). Overrides preserve the references, and they just work. No need to worry about the order of evaluation, how deep in the defaults the functions are, or any other implemention details.
  • Many more. More complete documentation coming soon.

Quick start

Install:

npm install config-one

Create a file called "config.js", with, for example:

module.exports = {
  'current-site': 'prod',
  sites: {
    dev: {
      port: 8870,
      path: '/home/acme/project', 
    },
    prod: {
      port: 80,
      path: '/var/acme/data',
    }
  }
};

Access this from your scripts with, for example:

var cfg = require('config-one')();

var siteConfig = cfg.sites[cfg['current-site']];
var port = siteConfig.port;   //=> 80

You can apply overrides from other sources at any time. By default, this library looks for and reads config values from files in the current process' working directory, and from environment variables.

You can specify an alternate directory to use as an option to require, or by setting the CONFIG1_DIR environment variable.

The sources it uses by default are:

  • config.json, config.js - default values
  • config-local.json, config-local.js - local overrides
  • Environment variables prefixed with CONFIG1_ - take precedence over files

For example, to run a development version of your app, you could create a file named "config-local.json" (which typically would not be included in version control) with:

{ "current-site": "dev",
  "sites": {
    "dev": { "port": 8880 }
  }
}

The port variable in the example above would then resolve to 8880.

You could override any of those local settings with environment variables. For example, if you ran with CONFIG1_sites.dev.port=9000, the example above would yield 9000.

Overrides and recipes

In your JavaScript configuration files, you can use functions (called "recipes") that specify how to derive some settings from others. For example, you could include the logic to select the highest-version library that matches a semver expression, right in your JavaScript configuration file:

// config.js

var C1 = require('config-one');
var semver = require('semver');

// Compose a function to select the best matching semver from a list
var sort = semvers => semvers.sort(semver.lt);
var match = vexpr => (ver => semver.satisfies(ver, vexpr));
var best = (semvers, vexpr) => sort(semvers).find(match(vexpr));

module.exports = {
  cdn: 'https://cdn.org/',
  libs: {
    available: {
      'markdown-it': {
        versions: [ '1.0.0', '1.1.0', '2.0.1' ],
      },
      // ... 
    },
    required: {   // specify libs using semver
      'markdown-it': '>=2.0.0',
      // ...
    },

    // Recipe to determine the enabled libraries
    enabled: C1(X=> { 
      var enabled = {};   // The recipe will return this object
      // References to other config items:
      var available = X.libs.available,  
          required = X.libs.required;

      Object.keys(required).forEach(key => {   
        var req = required[key],
            avail = available[key];

        if (!avail || !avail.versions) { throw Error('lib not available');  }

        // Find a matching version [FIXME: need `freeze` for now]
        var versions = C1.freeze(avail.versions);

        var winner = best(versions, req);
        if (!winner) { throw Error('no matching semver'); }
        enabled[key] = winner;
      });
      return enabled;
    })
  }
};

Putting these functions in the settings file allows you to override them at any step in the processing chain, and other settings that use those values will be correct. In other words, the semantic logic that ties the settings together does not get clobbered by the overrides.

More detailed documentation is coming soon, but for another example of what you can do, have a look at the options settings for this library itself, at src/defaults.js.

Note how the settings variables are normalized to eliminate redundancy, and logic that derives some settings from others is baked in, using recipes (sorry for the pun)! These recipes work without your having to fret about the order of evaluation, or how or when they are overridden.

API

var C1 = require('config-one');

// Read configuration data according to the current options
var cfg = C1();

// Make a new, permanent clone with custom options.
var C2 = C1.new(opts);

// Read config data from a set of source specifiers.
var cfg = C1.read(...sourceSpecifiers)

// Create a new view over the supplied config objects and/or views
var cfg = C1.extend(...configs);

// Create a new recipe (used inside configuration objects)
C1.recipe(<function>)

// Shortcut for .recipe() (one argument which is a function)
C1(<function>)

// Make a permanent, static deep copy of a configuration view
var opts = C1.freeze(cfg);

Building / development

You can turn on some verbose debugging messages in various places by setting the C1_DEBUG environment variable to "true". This will go away once we integrate a real logging framework.

To install all the needed dependencies:

npm install

Run unit tests against the original source files with:

npm test

To compile all the ES2015 (also known as EcmaScript 6) into more portable ES5, invoke babel with:

FIXME: when Babel is integrated with webpack, we should probably do away with this step.

npm run build     #=> outputs are written to build/

To generate the main cross-platform distribution bundle:

webpack        #=> dist/config-one.js:

The next sections give reference information and some how-tos

Directory and file conventions

The directory structure of this app looks like the following. This list is annotated with some rationale information.

/
  package.json
  webpack.config.js
  ...
  src/
  dist/  - just for final distribution packages
  test/
    ✓run.sh - just runs the mocha tests without bundling - this is currently
        test.sh. It is what is invoked by the `npm test` scripts.
    ✓index.html  - driver for the mocha tests in the browser, the main JS
        this invokes are ../build-{src|dest}/test-bundle.js.
    webpack.test.config.js
    check-spec.js  - and any other utility modules

    unit/         - sources for the unit tests
        main.js   - currently this is test/index.js
        <name>.test.js
        ...
    smoke/
        browser-test.html
        browser-test.js
    build/      - test bundles
        test-bundle-src.js
        test-bundle-dist.js
    reports/
        src/    - separate destination for each unit-under-test
        dist/

Testing

The unit test scripts use mocha/chai, and are in the test/unit/ subdirectory, with names that end with ".test.js".

Building and running the various combinations of tests and environments is controlled by two environment variables:

  • C1_UUT - defaults to 'src'
  • C1_TARGET - defaults to 'node'

FIXME: I don't thing TARGET actually makes sense here, since we are producing a single "UMD" bunde that runs anywhere.

A third variable can but used to turn on some verbose messages in places:

  • C1_DEBUG

Testing sources: $C1_UUT = "src"

As describe above, to run the unit tests against the original source files, from the command line, just enter

npm test

The test script command line is defined in package.json. It invokes the bash shell script test/run.sh. You can run it manually, without npm, if you need to:

test/run.sh

That shell script, in turn, is just a thin wrapper around this command:

mocha -R mochawesome --reporter-options \
  "reportDir=$REPORT_DIR,reportName=test-report" $TEST_FILES

In addition to reporting the test status on the console, this also produces a very informative HTML report at test/reports/src/test-report.html.

To run an individual test from the suite, just specify it by filename. For example:

mocha test/unit/low-level.test.js

Node.js platform, testing "dist"

This mode runs the same suite of tests against the final, bundled library. First make sure you build the distribution bundle (with webpack) and then, to run the tests:

npm run test-dist

This invokes the same script as above, test/test.sh, but with the C1_UUT environment variable set to "dist".

Browser smoke test

This doesn't use mocha, and doesn't run extensive unit testing. It is just a basic test to ensure that the distribution bundle can run in whatever browser you use.

Running this without using any runtime webpack components (such as the webpack-dev-server), provides an additional assurance that the bundle can be distributed.

To use it, make sure the distribution bundle has been build, then start a static server, with, for example:

http-server -p 9000

Then open http://localhost:9000/test/smoke/browser-test.html, and verify that you see a green "pass" indication on the page.

Viewing this smoke test page through the dev server allows it to update dynamically with every change to a source file.

webpack-dev-server

Then open http://localhost:8080/test/browser-smoke-test.html. (Note the port number is different!)

When there are changes to source files, the dev server will automatically rebuild the bundle and serve it from memory at the URL path /dist/config-one.js.

browser / src

As with a normal web application, there are two main ways of running the Mocha test suite through a browser: with a static bundle (in this case, it's a test bundle, that includes the library code bundle within it), or by using the webpack-dev-server.

To create the static bundle:

webpack --config test/webpack.test.config.js

Then, start your static web server, and bring up this page (assuming your server is on port 9000): http://localhost:9000/test/test.html.

The work with these tests using the dev server:

This is the best option for dynamically viewing test results as you develop. since this dev server instance will rebuild everything whenever there is any change to a library source file or to a test.

webpack-dev-server --config webpack.test.config.js

Then bring up http://localhost:8080/test/test.html. (If you want to change the dev server port number, you have to do it from the command line.)

browser / dist

This is nearly the same as the previous example. The only difference is that the test modules require the library from the distribution bundle, rather than directly from the sources.

To create the static bundle:

C1_UUT=dist webpack --config webpack.test.config.js

Then bring the file up in http://localhost:9000/test/test.html.

To view it through the webpack-dev-server, do the following (but note that this does not auto-rebuild when there are changes to the original source files, so this setup isn't recommended):

C1_UUT=dist webpack-dev-server --config webpack.test.config.js --hot

And then point your browser at http://localhost:8080/test/test.html.

Acknowledgements

node-config library

This is a fantastic package, with a very well-thought out API. I particularly like the default behavior, which makes it dead simple to use out of the box. (It would be nice if it were more configurable, though -- after all, it's a configuration manager!)

Mark Stosberg developed the "deferred" feature of that library, and described an algorithm (see this comment, under "Proposed design: lazy resolution using getters") that evolved into the one now used here.

Chainmap (Python recipe)

See this Python recipe, by Raymond Hettinger. This was where I first learned of the value of maintaining the chain of maps indefinitely, and rather than resolving them, to provide a view into them, that looks like a single map to the user.

Implementation, other notes

Are at config-one-project.

License

Public domain.

ODC Public Domain Dedication & License 1.0

0.0.5

7 years ago

0.0.4

7 years ago

0.0.3

7 years ago

0.0.2

8 years ago

0.0.1

8 years ago