0.3.1 • Published 4 years ago

typed-config-dsl v0.3.1

Weekly downloads
2
License
MIT
Repository
github
Last release
4 years ago

typed-config-dsl

Travis Coveralls Prettier

A focused, lightweight DSL for reading typed configuration from the environment.

Features

  • ✅ Clearly demarcate required, optional and optional with default valued keys
  • 🔟 Statically type and correctly parse typed values, e.g. numbers and booleans
  • 🚫 Strict format requirements no lenient parsing or ambiguous configuration
  • 📋 Analyze and provide feedback on the whole configuration on failure
  • 👀 Provide means for marking sensitive keys and mask them appropriately

The library works well in combination with another configuration loading utility, such as dotenv. Best taken advantage of with TypeScript, but even JavaScript projects benefit from its features.

Examples

Example usage

import { readConfig, string, number, boolean } from 'typed-config-dsl';

interface Config {
  server: {
    interface: string;
    port: number;
  };
  basicAuth: {
    enabled: boolean;
    username: string | undefined;
    password: string | undefined;
  };
}

const config: Config = readConfig({
  server: {
    interface: string('SERVER_INTERFACE'),
    port: number('SERVER_PORT').default(8080),
  },
  basicAuth: {
    enabled: boolean('AUTH_ENABLED').default(false),
    username: string('AUTH_USERNAME').optional(),
    password: string('AUTH_PASSWORD').optional().sensitive(),
  },
});

Example for valid config

Let's say the current environment would be as follows:

{
  "SERVER_INTERFACE": "0.0.0.0",
  "SERVER_PORT": "443",
  "AUTH_ENABLED": "true",
  "AUTH_USERNAME": "user",
  "AUTH_PASSWORD": "secret"
}

Then readConfig in the above example would succeed, and log the following message:

Configuration read:
 - SERVER_INTERFACE string  0.0.0.0
 - SERVER_PORT      number  443
 - AUTH_ENABLED     boolean true
 - AUTH_USERNAME    string  user
 - AUTH_PASSWORD    string  (sensitive)

And return the following value (represented here as JSON):

{
  "server": {
    "interface": "0.0.0.0",
    "port": 443
  },
  "basicAuth": {
    "enabled": true,
    "username": "user",
    "password": "secret"
  }
}

Example for invalid config

Let's say the current environment would instead be as follows:

{
  "SERVER_PORT": "443/https",
  "AUTH_ENABLED": "yes",
  "AUTH_USERNAME": "user",
  "AUTH_PASSWORD": "secret"
}

Then readConfig in the above example would throw an error with the following message:

Invalid configuration:
Missing values for following configuration keys:
 - SERVER_INTERFACE string
Malformed values for following configuration keys:
 - SERVER_PORT      number  443/https Badly formatted number
 - AUTH_ENABLED     boolean yes       Expected `true` or `false`

Notice the strict format requirement although parseFloat("443/https") === 443, the value is rejected. In the same vein, "yes" is a truthy value, but so is "no".

DSL

The DSL is split into top-level combinators, which define required readers, and an extension DSL which provides utilities for defining optional and sensitive configuration keys.

Top-level combinators

The top-level DSL consists of the constant, string, number, and boolean combinators, and array builder, which itself has combinators for string, number and boolean.

In addition, object literals with ConfigReader<A> leaf values can be used to read structured configuration. Such structures can be recursively nested.

constant<A>

ParameterTypeDefaultDescription
valueAn/aThe constant value to be returned by the reader.

Builds a reader for a constant value. This is useful when you want to hard code a value in your configuration object.

Example:

const reader: ConfigReader<number> = constant(42);

string

ParameterTypeDefaultDescription
keystringn/aThe key of the environment variable to read.

Builds a required reader for a string-valued configuration key.

Example:

const reader: ConfigReader<string> = string('FOO');

number

ParameterTypeDefaultDescription
keystringn/aThe key of the environment variable to read.

Builds a required reader for a number-valued configuration key. The value is parsed using readFloat and checked to not be NaN.

Example:

const reader: ConfigReader<number> = number('FOO');

boolean

ParameterTypeDefaultDescription
keystringn/aThe key of the environment variable to read.

Builds a required reader for a boolean-valued configuration key. The value is required to be either true or false.

Example:

const reader: ConfigReader<boolean> = boolean('FOO');

array

ParameterTypeDefaultDescription
keystringn/aThe key of the environment variable to read.
separatorstring,The delimiter token for string splitting.

An intermediate builder for array-valued configuration keys. Arrays are expected to be delimiter-separated string values and cannot be nested.

The value is parsed by splitting using string .split(separator) and then the resulting array values are parsed based on the element type combinator. Format requirements for the elements are the same as for the corresponding typed top-level combinators.

Example:

const reader1: ConfigReader<string[]> = array('FOO').string();
const reader2: ConfigReader<number[]> = array('BAR', ';').number();
const reader3: ConfigReader<boolean[]> = array('ZUT', '|').boolean();

Object literals

Object literals can be used to create structured config entries. These can be recursively nested. The leaf values of such structures must be readers, produced e.g. with the DSL combinators.

Example:

const reader = {
  foo: string('FOO'),
  bar: {
    baz: number('BAZ'),
    qux: boolean('QUX'),
  }
};

Extension combinators

These combinators exist as methods on a ConfigReader<A>. If a reader is both optional (or has default value) and sensitive, then sensitive must be the last combinator. This convention is enforced by the DSL typings for consistency.

optional

ParameterTypeDefaultDescription
...environmentstring[][]Names of environments in which the key is optional.

Marks the extended reader as optional. It's type A is extended statically to be A | undefined.

Example:

const reader: ConfigReader<number | undefined> = number('FOO').optional();

default

ParameterTypeDefaultDescription
defaultValueAn/aThe value to be used if the key is not present.
...environmentstring[][]Names of environments in which the key is optional.

Marks the extended reader as optional with a default value. It's static type remains unchanged.

Example:

const reader: ConfigReader<boolean> = boolean('FOO').default(true);

sensitive

No parameters.

Marks the extended reader as sensitive. Sensitive values are masked when the successfully read configuration is logged and in output for failure messages.

Example:

const reader: ConfigReader<string> = string('PASSWORD').sensitive();

Reading configuration

The DSL specifies a ConfigReader<A> for some type A. To actually read a value of type A from the environment, readConfig is used.

readConfig<A>

ParameterTypeDefaultDescription
readerConfigLikeReader<A>n/aThe reader that will be used to read a value of type A.
optionsOptionsdefaultOptionsOptions for reading the configuration.

Options, defaultOptions are as in the default column.

OptionTypeDefaultDescription
silentbooleanfalseIf true, don't log the successfully read configuration.
logger(message: string) => voidconsole.logA logger for logging messages.

Reads a reader of type A which is specified using the DSL.

If the configuration read from the environment matches the specification, the configuration is logged (with valuess marked as sensitive masked) and a value of type A is returned.

If the configuration is not matched, e.g. some required key is missing or some value cannot be correctly parsed, an error with a description message detailing the status of the whole configuration is thrown.