Skip to content

English__Theory__Per Trait Configuration

Ramirez Vargas, José Pablo edited this page Feb 5, 2023 · 3 revisions

Per-Trait Configuration

Per-Trait configuration is a specialization of conditional configuration and works by defining traits that are applied to the current environment. Then, data sources are included or excluded based on the presence of the various traits in the current environment.

This is probably a bit confusing right now, so let's re-introduce the topic: Have you ever had the need to create multiple versions of environment-specific configurations because something must be added that depends on region, customer type or some other piece of data? If so, per-trait configuration may be for you.

What Are Traits?

Traits are characteristics that can be pinned to the current environment, or if you are not working with environments at all, traits are characteristics that just exist in the application. The API's work with the traits defined for the current environment, so if your case is an environment-less deployment, simply define a single environment.

Programatically speaking, traits are either bitmasked numbers or strings. The recommendation is to use the former because they perform faster and are overall simpler to work with. Still, strings are fully supported.

Bitmasked Traits

NOTE: This document does not aim to explain what a bitmasked value is, and only provides a quick overview.

A bitmasked value is a value obtained by having a single bit lit (only one bit in it will be 1; all others will be 0).

The idea is to define all possible traits in constants somewhere/somehow, and you as developer are free to select the definition method you prefer. This documentation, however, declares traits using enumerations, like this one:

const myTraits = Object.freeze({
    None: 0x0,
    Trait1: 0x1,
    Trait2: 0x2,
    Trait3: 0x4,
    Trait4: 0x8,
    Trait5: 0x10,
    ... // etc.
});

The example is a typical enumeration definition in JavaScript using hexadecimal notation for the values. Each one of those values has only one bit lit. The enumeration would typically be exported as a module and imported wherever is needed.

The idea is to grant the current environment a numerical value for its traits property that contain all the traits the environment has. For example, the decimal value 7 (hexadecimal 0x7) has the 3 least significant bits lit, and is therefore the combination of Trait1, Trait2 and Trait3. Similarly a decimal value of 20 (hexadecimal 0x14) is the combination of Trait3 and Trait5.

String Traits

String traits do not have a corresponding mathematical way of grouping them, and consume more RAM than numerical traits because of this (because they need to be aggregated as an array). Furthermore, the testing algorithm is O(n) because the traits property is an array of strings that is searched sequeantially using the includes() function, whereas numerical traits will always consume 8 bytes only, and its testing algorithm is lightning fast in comparison.

The string counterpart of the previous example would be this:

const myTraits = Object.freeze({
    Trait1: 't1',
    Trait2: 't2',
    Trait3: 't3',
    Trait4: 't4',
    Trait5: 't5',
    ... // etc.
});

Assign Traits to the Current Environment

This is explained in full here and its example is reproduced here, including this time the parsing one would do for string traits:

import wjConfig, { Environment, EnvironmentDefinition } from "wj-config";
import mainConfig from './config.json';

// Numerical traits (someEnvironmentVariable = 7):
const myCurTraits = parseInt(someEnvironmentVariable);
// String traits (someEnvironmentVariable = 't1','t2','t3'):
const myCurTraits = JSON.parse(`[${someEnvironmentVariable}]`);
const curEnv = new EnvironmentDefinition('myCurrentEnvironment', myCurTraits);
// Pass the current environment definition as first argument.
const env = new Environment(curEnv, [
    'Dev',
    'PreProd',
    'Prod'
]);

const config = await wjConfig()
    .includeEnvironment(env)
    ...
    .build();

export default config;

Doing Per-Trait Configuration

Now that all the pre-requisites are covered, it is time to board the main topic, which is how to add data sources based on the existence of one or more environment traits.

The configuration builder provides 2 functions that join together the builder's when() function with the environment's hasAllTraits() and hasAnyTrait() functions: whenAllTraits() and whenAnyTrait().

Just like the when() function, whenAllTraits() and whenAnyTrait() apply to the last-added data source and they must be called immediately after adding the data source, or immediately after naming the data source. Unlike the when() function, the call to the builder's includeEnvironment() is required. If includeEnvironment() is not called, the builder's build() function will throw an error.

Just as the environment's hasAllTraits() returns true if the current environment's traits include all listed traits, applying whenAllTraits() will instruct the builder to only include the last-added data source if the current environment's traits include all of the listed traits.

A similar explanation applies to whenAnyTrait(), where the last-added data source is included if the current environment's traits include any of the listed traits.

This is how you would add a fetched data source if the current environment has traits 2 and 3:

import wjConfig, { Environment, EnvironmentDefinition } from "wj-config";
import myTraits from "./myTraits.js";
import mainConfig from "./config.json";

const envTraits = parseInt(window.env.APP_ENVTRAITS);
const ed = new EnvironmentDefinition(window.env.APP_ENVIRONMENT, envTraits);
const env = new Environment(ed, ['Dev', 'PreProd', 'Prod']);
const config = wjConfig()
    .addObject(mainConfig).name('Main')
    .addFetched('./config.T2-3.json').name('T2 + T3 Config')
    .whenAllTraits(myTraits.Trait2 | myTraits.Trait3)
    .includeEnvironment(env)
    .build();

export default await config;

The use of whenAllTraits() conditions the last-added data source (the fetched configuration named T2 + T3 config) to the presence of traits 2 and 3, which come defined in the myTraits enumeration.