Last active
April 25, 2019 17:36
-
-
Save hediet/7f22c73f2ff02eb0bc8095ab2691b102 to your computer and use it in GitHub Desktop.
Config System
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
// 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>>; |
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
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