@l.degener/irma-config

Configuration subsystem extracted from https://github.com/ldegen/irma


License
MirOS
Install
npm install @l.degener/irma-config@1.2.0

Documentation

Basic Usage

A rather trivial example would look like this


config = ConfigBuilder()
  .typePath [process.env.TYPE_PATH]
  .load pathToConfigFile
  .build()

The Configuration Program Monad

A configuration program comprises two things: a configuration and an action that makes use of that configuration. The action may be a no-op, it may also be a chain of several atomic steps. It's all the same to us.

The bind operator

The Configuration Builder mainly defines a monadic combinator bind that makes the set of configuration programs a monad.

First, let's assume that any instance of ConfigurationBuilder has a current configuration and a current action. It does not modify either of the two.

Now we call configBuilder.bind f for some Kleisli Arrow f. The operation will start by creating an intermediate instance tmpBuilder by simply applying f to the current configuration.

It will then create a new action combinedAction by chaining the current action of configBuilder and that of tmpBuilder, taking care of all the result-passing and promise-related shenanigans.

Finally it will take the current configuration of tmpBuilder and the combinedAction it just created, and wrap both up in a new ConfigBuilder. This is the return value.

Why is the current action not passed to the arrow functions?

The bind-operation only passes the current configuration to the arrows, but not the current action. Which is both good and bad.

It is good, because the chaining of the actions is taken care of by bind. so the arrows do not have to deal with this. My observation was that most of the basic arrow steps either modify the configuration or the action, but not both. If they did modify the action, it would usually be monotonic (i.e. append a step to a chain of actions). So moving the responsibility to the arrows would add repetitive clutter with little gain.

It is bad, because it effectively prohibits arrows from altering the action in non-monotonic ways (e.g. overriding or veto-ing of side effects). I have yet to encounter a case where this is a problem, but still -- it seems "incomplete".

I think I will have a second variant of the bind-operation for that. I could even examine the number of formal parameters of the arrow function to determine which of the variants to use.

Glossary

Configuration

As far as the config builder is concerned, a configuration is just a plain Javascript object. A builder will carry around some configuration object, allowing you to incrementally modify or extend it. When you finally call the .build()-method, it will put the configuration into a new instance of RootNode, which will traverse the configuration and take care of initializing any nested ConfigNode-instances in the correct order.

Environment

When executing a configuration program via .run(), the caller passes two (optional) arguments: The so-called environment and an optional initialization argument for the action. Both are treated similar in that they do not end up in the final configuration but instead are passed on to the action. They do however serve different purposes, which becomes clearer if we consider programs that contain an action composed of more than one step. In this case, each step will be called with the same environment. In contrast, the second argument is only passed to the first step. The second step instead sees the return value of the first step, the third that of the second and so on.

Action

Actions provide a way of adding side effects to your configuration program. An action is a javascript function that takes three arguments:

  • the environment,
  • the root node of the final configuration
  • an optional argument.

You may chain any number of actions to the program via the .then() method. When you execute the program (via .run()), these actions will be executed in the order they were attached. The optional third argument is used to pass the result of the previous step to the next one. The second argument of the .run() will be passed to the first action in the chain.

If your action involves asynchronous work, you probably want to have it return a promise object.

A rather canonic example for encoding side effects in a configuration program can be found in IRMa's CLI module. Depending on the options given on the command line -- it will either start the service, print help or generate a manpage when the configuration program is executed.

Configuration Program

A Configuration Program is just a pair of a configuration and an action. Keep in mind that actions are composable, so "one" action may in fact be composed of several atomic steps.

Config Programs are not directly accessible through the API, instead we use ConfigBuilder instances to manipulate and optionally/eventually execute them.

Configuration Builder

The Configuration Builder is an API that allows you to construct and eventually execute configuration programs. It does so by using what I like to call "monadic composition", though the term may not be accurate by mathematical standards.

Arrows

In the ConfigBuilder implementation you will find a group of functions/methods being refered to as arrows, hinting to fact that they either resamble Kleisli Arrows themselfs or are in fact higher-order functions producing Kleisli Arrows. This is just a fancy term for a very simple thing: A Kleisli Arrow is a function that takes a "plain" (i.e. non-monadic) value and produces a monadic value.

In our case, the "arrows" are functions that take a plain configuration object and return a new ConfigBuilder. It's the type of function one would pass to the .bind() method. The API includes predefined implementations for what we believe are very useful examples:

  • typePath for modifying the plugin resolution path

  • load and tryLoad for merging configuration files into the configuration program

  • add for programatically appending configuration directives

  • then for appending side effects

Since you would typically use those in conjunction with the .bind()-method anyway, the API has shortcut notations for exactly this. So for example instead of builder.bind(load(someFileName)), you can equivalently write builder.load(someFileName)).

Of course you can (and very often will) create your own, application-specific arrows. Think of the predefined ones as building-blocks to support that process.