Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend easyConfig API #14

Open
1 of 8 tasks
justsml opened this issue Nov 19, 2022 · 0 comments
Open
1 of 8 tasks

Extend easyConfig API #14

justsml opened this issue Nov 19, 2022 · 0 comments

Comments

@justsml
Copy link
Contributor

justsml commented Nov 19, 2022

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],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant