Skip to content

Instantly share code, notes, and snippets.

@hediet hediet/Config.ts
Last active Apr 25, 2019

Embed
What would you like to do?
Config System
// tslint:disable: no-shadowed-variable
/**
* Describes a new configuration.
*/
export function newConfig(): Config {
return new (Config as any)();
}
/**
* Represents a configuration description.
*/
export class Config<
TConfig extends ConfigType = {},
TAbstractFields extends string | number | symbol = keyof TConfig
> {
private constructor(
private readonly parent?: Config<any, any>,
private readonly vals: Record<
string,
(self: any, parent: any) => any
> = {},
private readonly configType?: ConfigType
) {}
/**
* Lists all abstract fields.
*/
public get TAbstractFields(): TAbstractFields {
throw "Don't call";
}
/**
* Gets the configuration type.
*/
public get TConfig(): TransformOut<TConfig> {
throw "Don't call";
}
/**
* Extends the configuration with abstract fields.
*/
public withConfigType<TAdditionalConfig extends ConfigType>(
config: TAdditionalConfig
): Config<
TConfig & TAdditionalConfig,
TAbstractFields | keyof TAdditionalConfig
> {
return new Config(this, {}, config);
}
private lookupField(field: string): ItemType | undefined {
if (this.configType) {
if (field in this.configType) {
return this.configType[field];
}
}
if (this.parent) {
return this.parent.lookupField(field);
}
return undefined;
}
/**
* Implements abstract and overrides non-abstract fields.
*/
public impl<
TImpl extends Partial<TransformIn<TConfig, TransformOut<TConfig>>>
>(impl: TImpl): Config<TConfig, Diff<TAbstractFields, keyof TImpl>>;
/**
* Implements an abstract or overrides a non-abstract field.
*/
public impl<TKey extends keyof TConfig>(
key: TKey,
val: TransformSingleIn<TConfig[TKey], TransformOut<TConfig>>
): Config<TConfig, Diff<TAbstractFields, TKey>>;
public impl(keyOrObj: any, val?: any): any {
if (arguments.length === 2) {
return this.impl({ keyOrObj: val } as any);
}
const obj: Partial<
TransformIn<TConfig, TransformOut<TConfig>>
> = keyOrObj;
const vals: Record<string, (self: any, parent: any) => any> = {};
for (const [field, def] of Object.entries(obj)) {
const type = this.lookupField(field);
if (!type) {
throw new Error(`Unknown field "${field}"`);
}
if (type.kind === 'prop') {
vals[field] = (typeof def === 'function'
? def
: () => def) as any;
} else {
throw new Error('Not supported yet');
}
}
return new Config(this, vals);
}
/**
* Creates a new configuration that first looks up in `config` and then in `this`.
*/
public extendWith<
TConfigExt extends ConfigType,
TAbstrFieldsExt extends string | number | symbol
>(
config: Config<TConfigExt, TAbstrFieldsExt>
): Config<
TConfig & TConfigExt,
| (TAbstractFields & TAbstrFieldsExt)
| Diff<TAbstractFields, keyof TConfigExt>
| Diff<TAbstrFieldsExt, keyof TConfig>
> {
let parent: Config<any, any> = this;
if (config.parent) {
parent = this.extendWith(config.parent);
}
return new Config(parent, config.vals, config.configType);
}
private _instantiate(self?: object): object {
const subobj: Record<string, any> = {};
if (!self) {
self = subobj;
}
let parent: object | null = null;
if (this.parent) {
parent = this.parent._instantiate(self);
}
if (parent) {
Object.setPrototypeOf(subobj, parent);
}
for (const [key, val] of Object.entries(this.vals)) {
subobj[key] = () => val(self, parent);
}
return subobj;
}
/**
* Instantiates this configuration.
* This requires all fields to be implemented, i.e. no abstract fields.
* Check `TAbstractFields` to see which field has no implementation.
*/
public instantiate(this: Config<any, never>): TransformOut<TConfig> {
return this._instantiate() as any;
}
}
/**
* Describes a new configuration type.
* Use `prop` and `fn`.
*/
export function newConfigType<TConfig extends ConfigType>(
config: TConfig
): TConfig {
return config;
}
/**
* Describes a property.
*/
export function prop<T>(): PropItem<T> {
return { TType: null!, kind: 'prop' };
}
/**
* Not supported yet.
*/
export function fn<T extends Function>(): FnItem<T> {
return { TType: null!, kind: 'fn' };
}
export interface FnItem<TFnType> {
TType: TFnType;
kind: 'fn';
}
export interface PropItem<TType> {
TType: TType;
kind: 'prop';
}
export type ItemType = FnItem<any> | PropItem<any>;
export type ConfigType = Record<string, ItemType>;
export type TransformSingleIn<T extends ItemType, TSelf> = {
fn: T['TType'];
prop: T['TType'] | ((self: TSelf, parent: TSelf) => T['TType']);
}[T['kind']];
export type TransformIn<T extends ConfigType, TSelf> = {
[TKey in keyof T]: TransformSingleIn<T[TKey], TSelf>
};
export type TransformSingleOut<T extends ItemType> = {
fn: T['TType'];
prop: () => T['TType'];
}[T['kind']];
export type TransformOut<T extends ConfigType> = {
[TKey in keyof T]: TransformSingleOut<T[TKey]>
};
type Diff<
T extends string | symbol | number,
U extends string | symbol | number
> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;
import { Config } from './Config';
export type ConfigGroupType = Record<string, Config<any, any>>;
export class ConfigError extends Error {}
export function getConfigs<
T1 extends ConfigGroupType,
T2 extends ConfigGroupType
>(
configs: [ConfigGroup<T1>, ConfigGroup<T2>],
keys: [string, string]
): [T1[keyof T1], T2[keyof T2]] {
// TODO: Check all keys at once!
return [configs[0].get(keys[0]), configs[1].get(keys[1])];
}
export class ConfigGroup<T extends ConfigGroupType> {
public readonly name: string;
private readonly configs: T;
public get TName(): keyof T {
throw "Don't call";
}
constructor(name: string, configs: T);
constructor(configs: T);
constructor(nameOrConfigs: any, configs?: any) {
if (arguments.length === 1) {
this.name = 'config';
this.configs = nameOrConfigs;
} else {
this.name = nameOrConfigs;
this.configs = configs;
}
}
public get<TKey extends keyof T>(val: TKey): T[TKey];
public get(config: string): T[keyof T];
public get(config: string): any {
const c = this.configs[config];
if (!c) {
throw new ConfigError(
`Unknown ${
this.name
} "${config}"!\nAvailable configs: ${this.getAvailableConfigStr()}.`
);
}
return c;
}
public getAvailableConfigs(): (keyof T)[] {
return Object.keys(this.configs);
}
public getAvailableConfigStr(): string {
return this.getAvailableConfigs()
.map(c => `"${c}"`)
.join(', ');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.