Skip to content

Instantly share code, notes, and snippets.

@thomhos
Last active September 27, 2018 10:22
Show Gist options
  • Save thomhos/34390134b41389414aa21fcd63b238c6 to your computer and use it in GitHub Desktop.
Save thomhos/34390134b41389414aa21fcd63b238c6 to your computer and use it in GitHub Desktop.
public and private configurables
// You can create a ConfigurableValue which is either 'private' or 'public'.
// When creating the configuration, you pass in the context. Based on that some values are undefined or not.
// Typescript will tell you which ones are available and which ones aren't.
import R from "ramda";
export type Country = string;
export type Language = string;
export type Environment = "production" | "staging" | "branch" | "development";
export type Visibility = "private" | "public";
export interface Context {
visibility: Visibility;
environment: Environment;
country: Country;
language: Language;
}
// A configurable value either 'private' or 'public'
// When running evaluate with context, you will receive the value or undefined
class ConfigurableValue<V extends Visibility, T> {
visibility: V;
_evaluate: (ctx: Context) => T;
constructor(visibility: V, evaluate: (ctx: Context) => T) {
this.visibility = visibility;
this._evaluate = evaluate;
}
evaluate(ctx: Context): T | undefined {
if (this.visibility === "public") {
return this._evaluate(ctx);
}
if (ctx.visibility === this.visibility) {
return this._evaluate(ctx);
}
return undefined;
}
chain<R>(fn: (value: T) => R | ConfigurableValue<V, R>) {
return new ConfigurableValue(this.visibility, ctx => {
return ConfigurableValue.of(
this.visibility,
fn(this.evaluate(ctx))
).evaluate(ctx);
});
}
static of<V extends Visibility, T>(
visibility: V,
value: T | ConfigurableValue<V, T>
) {
return value instanceof ConfigurableValue
? value
: new ConfigurableValue(visibility, _ctx => value);
}
}
// two helpers to easily create private or public values.
// Both take a string and fn
// TODO: allow template strings array
type CtxFn<R> = (ctx: Context) => R;
function publicValue<R>(arg: R | CtxFn<R>) {
return new ConfigurableValue("public", ctx =>
ConfigurableValue.of(
"public",
typeof arg === "function" ? arg(ctx) : arg
).evaluate(ctx)
);
}
function privateValue<R>(arg: R | CtxFn<R>) {
return new ConfigurableValue("private", ctx =>
ConfigurableValue.of(
"private",
typeof arg === "function" ? arg(ctx) : arg
).evaluate(ctx)
);
}
// Environment helper
// Returns a configurable value based on the environment
type EnvironmentFn<T> = (ctx: Context) => T;
type EnvironmentMap<T> = { [key in Environment]: T | EnvironmentFn<T> };
type EnvironmentMapWithDefault<T> = Partial<EnvironmentMap<T>> & { default: T };
function environment<V extends Visibility, T>(
visibility: V,
environmentMap: EnvironmentMap<T> | EnvironmentMapWithDefault<T>
) {
return new ConfigurableValue(visibility, ctx => {
const defaultValue = (environmentMap as EnvironmentMapWithDefault<T>)
.default;
const value = environmentMap[ctx.environment] || defaultValue;
return typeof value === "function" ? value(ctx) : value;
});
}
// When unwrapping a ConfigurableValue it can resolve to either the value or undefined
type Unwrapped<CV extends Visibility, T> = T extends ConfigurableValue<
infer V,
infer U
>
? V extends "private" ? (CV extends "public" ? undefined : U) : U
: T;
// A class to generate a configMap
// It has a create fn to resolve all values.
class Config<T, K extends keyof T> {
configurationMap: T;
constructor(configurationMap: T) {
this.configurationMap = configurationMap;
}
create<C extends Context>(
ctx: C
): { [K in keyof T]: Unwrapped<C["visibility"], T[K]> } {
return R.map(
(value: Unwrapped<C["visibility"], T[K]>) =>
value instanceof ConfigurableValue ? value.evaluate(ctx) : value,
this.configurationMap as {
[K in keyof T]: Unwrapped<C["visibility"], T[K]>
}
);
}
}
// Example config with some values
// Note: No nesting
const configuration = new Config({
locale: publicValue(ctx => `${ctx.language}_${ctx.country}`),
token: privateValue("asdasdasd"),
secret_token: environment("private", {
development: _ctx => "XXXXXXXXXX", // can also be a fn
branch: "XXXXXXXXXXX",
staging: "XXXXXXXXXXX",
production: "YYYYYYYYYYY"
}),
public_token: environment("public", {
default: "XXXXXXXXXX",
production: "YYYYYYYY"
})
});
// Create a public config, here the secret_token will be undefined
const browserConfig = configuration.create({
visibility: "public",
environment: "development",
country: "US",
language: "en"
});
// Create a private config, here the secret_token will be it's real value
const serverConfig = configuration.create({
visibility: "private",
environment: "production",
country: "US",
language: "en"
});
console.log(browserConfig);
console.log(serverConfig);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment