Skip to content
Aviv C edited this page Sep 22, 2017 · 7 revisions

ConfEagerJS Documentation

ConfEagerJs Build Status at Travis CI

ConfEagerJS is an eager, strongly-typed configuration library for JavaScript, designed to maximize runtime safety, while maintaining lightweight, easy integration and dynamic nature.

Getting Started

The Main Entities

ConfEagerJS library provides 3 main entities:

  • Configuration Class (ConfEager) which declare the properties we want to read into memory.
  • Configuration Property (ConfEagerProperty) which declares the actual name and type of each property.
  • Configuration Source (ConfEagerSource) which connects to a data source and populates the data into the configuration class properties.

To show this in action, let's start with super simple example in which we would like to read and validate 2 configuration properties from the environment variables.

First, let's define the properties we would like to read by declaring our Configuration Class, which contains the two Configuration Properties:

class LocalConfiguration extends ConfEager {
    readonly enableLogs = new ConfEagerProperties.Boolean();
    readonly logDirectory = new ConfEagerProperties.String();
}

This declares the properties we would like to read, their type and their property names. Everything here is fully customizable, and we've used boolean and string out-of-the-box properties (more on that later). Now in order to load them from the environment variables, we use the out-of-the-box EnvironmentVariables configuration source:

const source = new ConfEagerSources.EnvironmentVariables();
const localConfiguration = new LocalConfiguration();
source.bind(localConfiguration);

This will look for properties named enableLogs and logDirectory in the environment variables, if one of them is not found, it will throw a MissingPropertiesError. Then it maps the extracted values into types boolean and string, if one of them fails to parse due to illegal value, it will throw a IllegalPropertyValueError. Then it binds the localConfiguration instance to the source; This means that if the source changes, the in-memory values of localConfiguration properties gets immediately updated. This is not usful in this case of environment variables, but may very well be useful when using files as sources, or any other source that may get live updates.

Then in order to read the configuration:

const enableLogs = localConfiguration.enableLogs.get(); // returns a string
const logDirectory = localConfiguration.logDirectory.get(); // returns a boolean 

Few notes before moving on:

Customization

As mentioned above, by default, ConfEagerJS provides ConfEagerSource implementations which read environment variables. In order to read configuration from MySQL, for example, we may use an implementation like this one:

const mysql = require('mysql');

class ConfEagerMySQLSource extends ConfEagerSource {

    private readonly _data: {[key: string]: string};

    constructor(private readonly refreshInterval: number,
                private readonly host: string,
                private readonly user: string,
                private readonly password: string,
                private readonly database: string) {
        super();
        this._data = {};
    }

    load() {
        setInterval(() => this.load, this.refreshInterval);
        const __this = this;
        return new Promise<void>((resolve, rej) => {
            const connection = mysql.createConnection({
                host: __this.host,
                user: __this.user,
                password: __this.password,
                database: __this.database
            });
            connection.connect();
            connection.query('SELECT * AS config', (error: any, results: {[key: string]: string}[]) => {
                if (error) {
                    rej(error);
                }
                else {
                    for (const result of results) {
                        __this._data[result["key"]] = result["value"];
                    }
                    __this.notifyUpdate();
                    resolve();
                }
            });
        });
    }

    protected get(path: string[]): string | null | undefined {
        return this._data[path[0]];
    }

}

Then in order to use it we can do:

const source = new ConfEagerMySQLSource(30000, "localhost", "username", "password", "db");
const localConfiguration = new LocalConfiguration();
source.load().then(() => source.bind(localConfiguration)});

Few notes before moving on:

  • The same way, we may use any other source, such as local files, Consul KV, ZooKeeper, Redis or any other local or remote source we like.
  • This is just an example implementation, it may be implemented using any other specific client and in any other way, as long as we fully initialize the data before calling bind method.
  • Note that the database information is needed to connect; This may be taken from some local configuration source, e.g. environment variables, local configuration file, etc. We just need to define another configuration class with some local configuration source, initialize it, and then use the resulted configuration to connect to the remote configuration source here. This actually is considered a good practice, in which we separate our local configuration (i.e. paths on current machine, machine identity, etc...) from cluster configuration (i.e. business logic parameters, or other configuration that relates to other nodes in the cluster).
  • Note that this implementation support updates using interval. A better implementation would involve some kind of mechanism to subscribe for updates. When we want to trigger an update for all bound configuration instances we may call the inherited notifyUpdate() method. More on that in ConfEagerSource section.

In More Detail

Configuration Class

A ConfEager configuration class is denoted by the class ConfEager. The purpose of this class is to declare the properties we want to read.

This is done by extending ConfEager, and adding property class fields. Preferably, property fields should be declared public readonly so that no getter methods will be needed and no outside modification may be possible.

Environment

Additionally, a prefix path may be set to all of the property keys by overriding pathKeys method. This may be used to tell confeager to lookup property names in a specified path in the source. If for example the source is a YAML file that looks like that:

dataCenter1:
 staging:
   url: http://staging.dc1.example.com
 production:
   url: http://production.dc1.example.com
dataCenter2:
 staging:
   url: http://staging.dc2.example.com
 production:
   url: http://production.dc2.example.com

and we would like to host the value of dataCenter1's staging, we may use pathKeys to return ["dataCenter1", "staging"] and have a single property called url, then we would get the value of dataCenter1.staging.url

Configuration Property

A ConfEager configuration property is denoted by the class ConfEagerProperty The purpose of this class is to declare the actual name and type of each property.

Required vs. Optional Properties

By default, a configuration property is required, and if missing from the source, a MissingPropertiesError will be thrown. To denote a property as optional, we may call the withDefaultValue method:

class Configuration extends ConfEager {
    readonly enableLogs = new ConfEagerProperties.Boolean().withDefaultValue(true);
}

At this point, if enableLogs property is missing from the source, it will receive the value of true.

Property Keys

By default, a configuration property value is looked up in the source by the name of the property field. For example, if we declare:

class Configuration extends ConfEager {
    readonly enableLogs = new ConfEagerProperties.Boolean();
}

then the source data is scanned to contain a property named enableLogs. To override it, we need to pass a different property name to the property constructor using the withPropertyName method:

class Configuration extends ConfEager {
    readonly enableLogs = new ConfEagerProperties.Boolean().withPropertyName("ENABLE_LOGS");
}

At this point, the source will be scanned for a property named ENABLE_LOGS, and it's values will be populated and bound to the enableLogs property field.

Property Types

Property values are extracted from the source as strings. The responsibility of parsing this string to the actual property value lies upon the ConfEagerPropery. For instance, if we have a boolean property, then valid values of the extracted string may be "true", "false", "0" or "1". Any other case should not be accepted. This is done through the ConfEagerPropery map(value: string): T method. This means that we can actually create a property class for any type we want. For example, easily implement a URL property:

class URLProperty extends ConfEagerProperty<string> {

    @Override
    protected URL map(String value) throws Exception {
        return new URL(value);
    }

}

Now when we do:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyURL someURL = new ConfEagerPropertyURL();
}

and then bind this configuration to a source, we actually validate the someURL property to be a valid URL, then we can read it's value using configuration.someURL.get();.

This way we may implement any property type we want, and parse them in any way we want.

Note that in order to support optional properties and explicit property keys, we need to inherit all of ConfEagerProperty constructors:

public class ConfEagerPropertyURL extends ConfEagerProperty<URL> {

    public ConfEagerPropertyURL(ConfEager.DefaultValue<URL> defaultValue, ConfEager.PropertyName propertyName) {
        super(defaultValue, propertyName);
    }

    public ConfEagerPropertyURL(ConfEager.PropertyName propertyName, ConfEager.DefaultValue<URL> defaultValue) {
        super(propertyName, defaultValue);
    }

    public ConfEagerPropertyURL(ConfEager.PropertyName propertyName) {
        super(propertyName);
    }

    public ConfEagerPropertyURL(ConfEager.DefaultValue<URL> defaultValue) {
        super(defaultValue);
    }

    public ConfEagerPropertyURL() {}

    @Override
    protected URL map(String value) throws Exception {
        return new URL(value);
    }

}
Out-of-the-Box Property Types

ConfEager provides out-of-the-box property types for all Java primitives (boolean, double, float, int, long) and their arrays (boolean[], double[], float[], int[], long[]), String and String[] and for any enum. All of those are available under ConfEagerProperty*.

To use enums we can do:

public class Configuration extends ConfEager {
    public final ConfEagerPropertyEnum<Example> example = new ConfEagerPropertyEnum<>(Example.class, false);
    public enum Example {
        VALUE1, VALUE2
    }
}

Note the false value on the constructor call which denotes whether or not to enforce case sensitivity on the extracted value.

Configuration Source

A ConfEager configuration source is denoted by the class ConfEagerSource. The purpose of this class is to connect to a data source and to populate the data into the configuration class properties.

To implement a custom configuration source, one must implement a single method String getValueOrNull(String propertyName):

public class ConfEagerCustomSource extends ConfEagerSource {

    @Override
    public String getValueOrNull(String propertyName) {
        return "value";
    }

}

This is a dummy source which return the value "value" for every property name. This way, it's very simple to connect to any local or remove data source and retrieve all it's values. Then, when getValueOrNull is called, return the matching value, or null if no value is found. A MySQL source implementation example can be found above.

Updating Data

Configuration sources support live updating of data, and this is up for the implementer to manage. Upon the updating of data, the inherited notifyUpdate() method must be called in order to propagate the updates to all bound configuration instances.

For example, if we want to use a pub/sub client which provides a registerForUpdates() method, we can do:

public class ConfEagerCustomSource extends ConfEagerSource {

    private Map<String, String> data;

    private Client client;

    public ConfEagerCustomSource() {
        client = ...
        data = client.getData();
        client.registerForUpdates(() -> {
            data = client.getData();
            this.notifyUpdate();
        });
    }

    @Override
    public String getValueOrNull(String propertyName) {
        return data.get(propertyName);
    }

}

Note how we must first update the data, and then call the notifyUpdate() method.

Similarly, we can implement a scheduled task to be executed at a constant interval, extract all data and the notify update.

Binding

Once we've initialized all the data in the source, we may use the source to populate and bind any configuration class. This is done by calling either <T extends ConfEager> T bind(Class<T> confEagerObjectClass) or void bind(ConfEager confEagerObject) methods. For example:

public class LocalConfiguration extends ConfEager {
    public final ConfEagerPropertyBoolean enableLogs = new ConfEagerPropertyBoolean();
}

and then either:

LocalConfiguration localConfiguration = source.bind(LocalConfiguration.class);

or:

LocalConfiguration localConfiguration = new LocalConfiguration();
source.bind(localConfiguration);

The first method is the recommended one, but it may be used only in case where the ConfEager class has an empty constructor. In any other case, we must manually initialize it, and then use the second method.

Out-of-the-Box Configuration Sources

ConfEager currently provides 3 out-of-the-box configuration sources:

  • ConfEagerSourceSystemProperties.INSTANCE which read data from the process System Properties.
  • ConfEagerSourceEnvironmentVariables.INSTANCE which read data from the Environment Variables.
  • ConfEagerSourceCombinator which receive other sources, and chain them one after the other for each property, until it is found in either of them.