Configuration subsystem extracted from https://github.com/ldegen/irma
Homepage Repository npm Download
npm install @l.degener/irma-config@1.2.0
A rather trivial example would look like this
config = ConfigBuilder()
.typePath [process.env.TYPE_PATH]
.load pathToConfigFile
.build()
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.
bind
operatorThe 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.
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.
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.
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.
Actions provide a way of adding side effects to your configuration program. An action is a javascript function that takes three arguments:
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.
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.
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.
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.