Skip to content

Extend easyConfig API #14

Description

@justsml

Extend easyConfig API

  • Add many examples, using easyConfig & autoConfig.
  • Consider renaming this repo generically to @elite-libs/config, and seamlessly combine the 2 use case/interfaces.
    • Look into combining the usage patterns of easyConfig & autoConfig (where each option is either defined using an object, or Array<string | Function> w/ optional last-position validation function.)
  • Alternatively, maintain 2 awkwardly differing modules for a bit longer - closer to v2.
    • autoConfig rename to cliConfig, configMap, appConfig, (should this be default export anyway?)
    • easyConfig, keep name? Might change to: appConfig?
  • Make sure the JSDoc content shows up where it's needed. (Not primarily on types, internals.)
  • Current easyConfig doesn't support the optional formatter function in tail position.

Current EasyConfig

  • Minimalist way to centralize your app configuration, handling both Argument & Environment key-value mapping.

Limits: Everything is a string. No built-in Zod validation like auto-config. No nesting, no arrays, no booleans, no numbers.

export const config = easyConfig({
  "url": ['--url', '-u', 'APP_URL'],
  "port": ['--port', '-p', 'PORT'],
  "debug": ['--debug', '-D'],
});

Well, it lives up to the name at least!

Proposed API Changes

Validator function interface:

type ValidatorFunction<TReturnType> = (
  value: string,     // The value to validate: '08080'
  sourceKey: string, // `--port`
  fieldName: string, // `port`
  allMatchedKeys: Record<string, string> // { 'PORT': '8080', '--port': '08080' };
) => TReturnType;

Example Validators

To support custom logic while getting types for free we'll support a trailing function pattern.

After the list of input args & env keys, include a ValidatorFunction as the last array element.

export const config = easyConfig({
  "debug": ['--debug', '-D', Boolean],
  "url": ['--url', '-u', 'APP_URL', url => new URL(url)],
});
config.debug; // boolean
config.url; // URL instance

Tip 1: Auto-type keys using JS' built-in methods Boolean(), Number(), String(), etc

A quick way to add type hints: use the language's primitive constructors.

export const config = easyConfig({
  "port": ['--port', '-p', Number],
  "debug": ['--debug', '-D', Boolean],
});

config.port; // number
config.debug; // boolean

ProTip 2: Convert inputs into advanced types

This snippet takes a url string and returns a new URL() instance.

If an error is encountered, the app will prevent invalid runtime states by exiting & re-throwing the original error message!

// Example used anonymous arrow function, but you can use a named function too.
const parseUrl = (url: string) => new URL(url);

ProTip 3: Parse JSON input strings

Note: Advanced example. Feel free to skip & revisit later.

The following example is a fair bit more complex.
The goal is showing a 'real-world' JSON parsing pattern evolves with this library.

Since arguments and environment variables can support long strings, it can sometimes be a convenient strategy to use stringified JSON. For example adding/removing features is easier if you know that theres only 1 place it's configured. A related issue: most enterprise apps eventually have to manage dozens to 100+'s of environment & configuration variables. The sprawl can quickly be a drag to manage and difficult to trace when things go wrong.

Let's visualize combining 2 keys into PRODUCT_PAGE={json}.

Given

  • PRODUCT_PAGE_SCROLL=infinite
  • PRODUCT_PAGE_LIMIT=10

We want to combine them into a single JSON key:

  • PRODUCT_PAGE={"scroll": "infinite", "limit": 10}

Future product additions can be added to the JSON with relative ease:

Let's say, your legal team has requested a new disclaimer be added to the product page.

We might have named this DISCLAIMER_PRODUCT, which could easily be overlooked when sorting and looking for PRODUCT_* in a sea of keys.

  • PRODUCT_PAGE={"scroll": "infinite", "limit": 10, "disclaimer": "..."}
  • SUGGESTIONS_PAGE={"scroll": "none", "limit": 10, "disclaimer": "(items other shoppers purchased)"}

When adding legal text to a website, it may be more important to see the disclaimers co-located in one key.

  • DISCLAIMERS={"product": "...", "home": "all rights reversed", "partner_content": "rights owned by their respective owners"}

Design to make features more serviceable & manageable. ✨

Let's look at more examples of JSON embedded keys.

export DATADOG_CONFIGS='{"apiKey":"my-key","sampling":"0.25","tags":["env:prod"]}'
export ACTIVE_FEATURES='["checkout_v4","cta_ab_test_v55","rewards_v3"]'

The following example shows how to tie this all together:

export const config = easyConfig({
  "datadog": ['DATADOG_CONFIGS', safeJsonParser],
  "features": ['--active-features', 'ACTIVE_FEATURES', safeJsonParser],
});

Example Strict & Safe JSON Parsers

// Throws on invalid JSON.
const strictJsonParser = (json: string) => JSON.parse(json);

// Invalid JSON will get logged, and the app will return false.
const safeJsonParser = (json: string, sourceKey: string) => {
  try {
    if (!json || json.length < 2) return false;
    return JSON.parse(json);
  } catch (error) {
    console.error('Invalid JSON supplied', error, {json, sourceKey});
    // NOTE: Change your default fall-back json value: instead of `false`, maybe `null` or an empty object or array might make more sense.
    return false; 
  }
};

4. ProTip: Configurable Port number validator

// Using the curried `portRangeChecker()` helper (see below)
const isHttpIsh = portRangeChecker({min: 80, max: 443});
// The func `isHttpIsh(port)` accepts a number, and if outside the range provided it'll fail, and if inside the.
isHttpIsh(79) //-> false
isHttpIsh(80) //-> true
isHttpIsh(444) //-> false
isHttpIsh(79) //-> false
const portRangeChecker = ({min = 1_000, max = 60_000}: PortRange) =>
  (port: number | string) => {
    port = parseInt(`${port}`);
    if (port > 65_535) throw Error(`Port number ${port} can't exceed 65535`);
    if (port < 1) throw Error(`Port number ${port} must be at least 1`);
    if (port >= min && port <= max) return port;
    throw new Error(`Unexpected Port ${port} value detected.`);
  }

type PortRange = {min: number, max: number};

5. ProTip: Zod-based type validation

// Use a Zod schema to validate, type check any config values.
const parseAcctId = input => zod.any(input).refine(Number).safeParse(input)?.data;

export const config = easyConfig({
  "accountId": ['--accountId', 'ACCOUNT_ID', parseAcctId],
});

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions