Last active
September 27, 2018 10:22
-
-
Save thomhos/34390134b41389414aa21fcd63b238c6 to your computer and use it in GitHub Desktop.
public and private configurables
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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