Skip to content

Commit

Permalink
feat: Type dictionaries and single values
Browse files Browse the repository at this point in the history
  • Loading branch information
webJose committed Nov 7, 2024
1 parent 49bf046 commit 7d02005
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 33 deletions.
15 changes: 8 additions & 7 deletions src/builders/Builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FetchedDataSource } from "../dataSources/FetchedDataSource.js";
import { JsonDataSource } from "../dataSources/JsonDataSource.js";
import { ObjectDataSource } from "../dataSources/ObjectDataSource.js";
import { SingleValueDataSource } from "../dataSources/SingleValueDataSource.js";
import type { ConfigurationValue, IBuilder, IDataSource, IEnvironment, IncludeEnvironment, MergeResult, Predicate, ProcessFetchResponse, UrlBuilderSectionWithCheck } from "../wj-config.js";
import type { ConfigurationValue, IBuilder, IDataSource, IEnvironment, IncludeEnvironment, InflateDictionary, InflateKey, MergeResult, Predicate, ProcessFetchResponse, UrlBuilderSectionWithCheck } from "../wj-config.js";
import { BuilderImpl } from "./BuilderImpl.js";
import { EnvAwareBuilder, type IEnvironmentSource } from "./EnvAwareBuilder.js";

Expand All @@ -20,12 +20,13 @@ export class Builder<T extends Record<string, any> = {}> implements IBuilder<T>
return this.add(new ObjectDataSource(obj));
}

addDictionary<NewT extends Record<string, any>>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator: string = ':', prefixOrPredicate?: string | Predicate<string>) {
return this.add<NewT>(new DictionaryDataSource(dictionary, hierarchySeparator, prefixOrPredicate));
addDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string = ':'>(dictionary: TDic | (() => Promise<TDic>), hierarchySeparator?: TSep, prefixOrPredicate?: string | Predicate<string>) {
return this.add<Exclude<InflateDictionary<TDic, TSep>, unknown>>(new DictionaryDataSource(dictionary, hierarchySeparator ?? ':', prefixOrPredicate));
}

addEnvironment<NewT extends Record<string, any>>(env: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), prefix: string = 'OPT_') {
return this.add<NewT>(new EnvironmentDataSource(env, prefix));
addEnvironment<TDic extends Record<string, ConfigurationValue>, TPrefix extends string = 'OPT_'>(env: TDic | (() => Promise<TDic>), prefix: string = 'OPT_') {
// @ts-expect-error InflateDictionary's resulting type, for some reason, always asserts true against "unknown". TS bug?
return this.add<InflateDictionary<TDic, '__', TPrefix>>(new EnvironmentDataSource(env, prefix));
}

addFetched<NewT extends Record<string, any>>(input: URL | RequestInfo | (() => Promise<URL | RequestInfo>), required: boolean = true, init?: RequestInit, procesFn?: ProcessFetchResponse<NewT>) {
Expand All @@ -36,8 +37,8 @@ export class Builder<T extends Record<string, any> = {}> implements IBuilder<T>
return this.add<NewT>(new JsonDataSource(json, jsonParser, reviver));
}

addSingleValue<NewT extends Record<string, any>>(path: string | (() => Promise<[string, ConfigurationValue]>), valueOrHierarchySeparator?: ConfigurationValue | string, hierarchySeparator?: string) {
return this.add<NewT>(new SingleValueDataSource<NewT>(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
addSingleValue<TKey extends string, TValue extends ConfigurationValue, TSep extends string = ':'>(path: TKey | (() => Promise<[TKey, TValue]>), valueOrHierarchySeparator?: TValue | TSep, hierarchySeparator?: TSep) {
return this.add<InflateKey<TKey, TValue, TSep>>(new SingleValueDataSource<InflateKey<TKey, TValue, TSep>>(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
}

name(name: string) {
Expand Down
15 changes: 8 additions & 7 deletions src/builders/EnvAwareBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FetchedDataSource } from "../dataSources/FetchedDataSource.js";
import { JsonDataSource } from "../dataSources/JsonDataSource.js";
import { ObjectDataSource } from "../dataSources/ObjectDataSource.js";
import { SingleValueDataSource } from "../dataSources/SingleValueDataSource.js";
import type { ConfigurationValue, IDataSource, IEnvAwareBuilder, IEnvironment, MergeResult, Predicate, ProcessFetchResponse, Traits, UrlBuilderSectionWithCheck } from "../wj-config.js";
import type { ConfigurationValue, IDataSource, IEnvAwareBuilder, IEnvironment, InflateDictionary, InflateKey, MergeResult, Predicate, ProcessFetchResponse, Traits, UrlBuilderSectionWithCheck } from "../wj-config.js";
import { BuilderImpl } from "./BuilderImpl.js";

export interface IEnvironmentSource<TEnvironments extends string> {
Expand Down Expand Up @@ -33,12 +33,13 @@ export class EnvAwareBuilder<TEnvironments extends string, T extends Record<stri
return this.add(new ObjectDataSource(obj));
}

addDictionary<NewT extends Record<string, any>>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator: string = ':', prefixOrPredicate?: string | Predicate<string>) {
return this.add<NewT>(new DictionaryDataSource(dictionary, hierarchySeparator, prefixOrPredicate));
addDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string = ':'>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator: string = ':', prefixOrPredicate?: string | Predicate<string>) {
// @ts-expect-error
return this.add<InflateDictionary<TDic, TSep>>(new DictionaryDataSource(dictionary, hierarchySeparator, prefixOrPredicate));
}

addEnvironment<NewT extends Record<string, any>>(env: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), prefix: string = 'OPT_') {
return this.add<NewT>(new EnvironmentDataSource(env, prefix));
addEnvironment<TDic extends Record<string, ConfigurationValue>, TPrefix extends string = 'OPT_'>(env: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), prefix: string = 'OPT_') {
return this.add<MergeResult<T, InflateDictionary<TDic, '__', TPrefix>>>(new EnvironmentDataSource(env, prefix));
}

addFetched<NewT extends Record<string, any>>(input: URL | RequestInfo | (() => Promise<URL | RequestInfo>), required: boolean = true, init?: RequestInit, procesFn?: ProcessFetchResponse<NewT>) {
Expand All @@ -49,8 +50,8 @@ export class EnvAwareBuilder<TEnvironments extends string, T extends Record<stri
return this.add<NewT>(new JsonDataSource(json, jsonParser, reviver));
}

addSingleValue<NewT extends Record<string, any>>(path: string | (() => Promise<[string, ConfigurationValue]>), valueOrHierarchySeparator?: ConfigurationValue | string, hierarchySeparator?: string) {
return this.add<NewT>(new SingleValueDataSource<NewT>(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
addSingleValue<TKey extends string, TValue extends ConfigurationValue, TSep extends string = ':'>(path: TKey | (() => Promise<[TKey, TValue]>), valueOrHierarchySeparator?: TValue | TSep, hierarchySeparator?: TSep) {
return this.add(new SingleValueDataSource<MergeResult<T, InflateKey<TKey, TValue, TSep>>>(path, valueOrHierarchySeparator, typeof path === 'function' ? valueOrHierarchySeparator as string : hierarchySeparator));
}

name(name: string) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type IBuilder } from "./wj-config.js";

export * from "./buildEnvironment.js";
export * from "./EnvironmentDefinition.js";
export type * from "./wj-config.js";
export default function wjConfig(): IBuilder {
return new Builder();
}
41 changes: 30 additions & 11 deletions src/wj-config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,33 @@ export interface ConfigurationNode {
/**
* Types an object-merging operation's result.
*/
export type MergeResult<T extends Record<string, any>, NewT extends Record<string, any>> = (Omit<T, keyof NewT> & {
export type MergeResult<T extends Record<string, any>, NewT> = (Omit<T, keyof NewT> & {
[K in keyof NewT]-?: K extends keyof T ?
(
T[K] extends Record<string, any> ?
(NewT[K] extends Record<string, any> ? MergeResult<T[K], NewT[K]> : never) :
(NewT[K] extends Record<string, any> ? never : NewT[K])
) : NewT[K]
}) extends infer R ? { [K in keyof R]: R[K] } : never;

/**
* Types individual dictionary values and inflates them.
*/
export type InflateKey<TKey extends string, TValue extends ConfigurationValue, TSep extends string, TPrefix extends string = ""> = TKey extends `${TPrefix}${infer FullKey}` ?
FullKey extends `${infer Key}${TSep}${infer Rest}` ?
{
[K in Key]: InflateKey<Rest, TValue, TSep>
} :
{
[K in TKey]: TValue;
} : never;

/**
* Inflates entire dictionaries into their corresponding final objects.
*/
export type InflateDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string, TPrefix extends string = ""> = {
[K in keyof TDic]: (x: InflateKey<K & string, TDic[K], TSep, TPrefix>) => void
} extends Record<keyof TDic, (x: infer I) => void> ? I : never;

/**
* Defines the shape of dictionaries.
Expand Down Expand Up @@ -133,7 +152,7 @@ export interface IBuilder<T extends Record<string, any> = {}> {
* prefix is always removed after the dictionary is processed. If no prefix is provided, then all dictionary
* entries will contribute to the configuration data.
*/
addDictionary<NewT extends Record<string, any>>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator?: string, prefix?: string): IBuilder<MergeResult<T, NewT>>;
addDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string = ':'>(dictionary: TDic | (() => Promise<TDic>), hierarchySeparator?: TSep, prefix?: string): IBuilder<MergeResult<T, InflateDictionary<TDic, TSep>>>;
/**
* Adds the specified dictionary to the collection of data sources that will be used to build the configuration
* object.
Expand All @@ -143,7 +162,7 @@ export interface IBuilder<T extends Record<string, any> = {}> {
* @param predicate Optional predicate function that is called for every property in the dictionary. Only when
* the return value of the predicate is true the property is included in configuration.
*/
addDictionary<NewT extends Record<string, any>>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator?: string, predicate?: Predicate<string>): IBuilder<MergeResult<T, NewT>>;
addDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string = ':'>(dictionary: TDic | (() => Promise<TDic>), hierarchySeparator?: TSep, predicate?: Predicate<string>): IBuilder<MergeResult<T, InflateDictionary<TDic, TSep>>>;
/**
* Adds the qualifying environment variables to the collection of data sources that will be used to build the
* configuration object.
Expand All @@ -153,7 +172,7 @@ export interface IBuilder<T extends Record<string, any> = {}> {
* prefix is always removed after processing. To avoid exposing non-application data as configuration, a prefix
* is always used. If none is specified, the default prefix is OPT_.
*/
addEnvironment<NewT extends Record<string, any>>(env: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), prefix?: string): IBuilder<MergeResult<T, NewT>>;
addEnvironment<TDic extends Record<string, ConfigurationValue>, TPrefix extends string = 'OPT_'>(env: TDic | (() => Promise<TDic>), prefix?: TPrefix): IBuilder<MergeResult<T, InflateDictionary<TDic, '__', TPrefix>>>;
/**
* Adds a fetch operation to the collection of data sources that will be used to build the configuration object.
* @param url URL to fetch.
Expand Down Expand Up @@ -185,13 +204,13 @@ export interface IBuilder<T extends Record<string, any> = {}> {
* @param value Value of the property.
* @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
*/
addSingleValue<NewT extends Record<string, any>>(path: string, value?: ConfigurationValue, hierarchySeparator?: string): IBuilder<MergeResult<T, NewT>>;
addSingleValue<TKey extends string, TValue extends ConfigurationValue, TSep extends string = ':'>(path: TKey, value?: TValue, hierarchySeparator?: TSep): IBuilder<MergeResult<T, InflateKey<TKey, TValue, TSep>>>;
/**
* Adds a single value to the collection of data sources that will be used to build the configuration object.
* @param dataFn Function that returns the [key, value] tuple that needs to be added.
* @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
*/
addSingleValue<NewT extends Record<string, any>>(dataFn: () => Promise<[string, ConfigurationValue]>, hierarchySeparator?: string): IBuilder<MergeResult<T, NewT>>;
addSingleValue<TKey extends string, TValue extends ConfigurationValue, TSep extends string = ':'>(dataFn: () => Promise<readonly [TKey, TValue]>, hierarchySeparator?: TSep): IBuilder<MergeResult<T, InflateKey<TKey, TValue, TSep>>>;
/**
* Sets the data source name of the last data source added to the builder.
* @param name Name for the data source.
Expand Down Expand Up @@ -250,7 +269,7 @@ export interface IEnvAwareBuilder<TEnvironments extends string, T extends Record
* prefix is always removed after the dictionary is processed. If no prefix is provided, then all dictionary
* entries will contribute to the configuration data.
*/
addDictionary<NewT extends Record<string, any>>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator?: string, prefix?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, NewT>>;
addDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string = ':'>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator?: string, prefix?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, InflateDictionary<TDic, TSep>>>;
/**
* Adds the specified dictionary to the collection of data sources that will be used to build the configuration
* object.
Expand All @@ -260,7 +279,7 @@ export interface IEnvAwareBuilder<TEnvironments extends string, T extends Record
* @param predicate Optional predicate function that is called for every property in the dictionary. Only when
* the return value of the predicate is true the property is included in configuration.
*/
addDictionary<NewT extends Record<string, any>>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator?: string, predicate?: Predicate<string>): IEnvAwareBuilder<TEnvironments, MergeResult<T, NewT>>;
addDictionary<TDic extends Record<string, ConfigurationValue>, TSep extends string = ':'>(dictionary: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), hierarchySeparator?: string, predicate?: Predicate<string>): IEnvAwareBuilder<TEnvironments, MergeResult<T, InflateDictionary<TDic, TSep>>>;
/**
* Adds the qualifying environment variables to the collection of data sources that will be used to build the
* configuration object.
Expand All @@ -270,7 +289,7 @@ export interface IEnvAwareBuilder<TEnvironments extends string, T extends Record
* prefix is always removed after processing. To avoid exposing non-application data as configuration, a prefix
* is always used. If none is specified, the default prefix is OPT_.
*/
addEnvironment<NewT extends Record<string, any>>(env: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), prefix?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, NewT>>;
addEnvironment<TDic extends Record<string, ConfigurationValue>, TPrefix extends string = 'OPT_'>(env: Record<string, ConfigurationValue> | (() => Promise<Record<string, ConfigurationValue>>), prefix?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, InflateDictionary<TDic, '__', TPrefix>>>;
/**
* Adds a fetch operation to the collection of data sources that will be used to build the configuration object.
* @param url URL to fetch.
Expand Down Expand Up @@ -302,13 +321,13 @@ export interface IEnvAwareBuilder<TEnvironments extends string, T extends Record
* @param value Value of the property.
* @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
*/
addSingleValue<NewT extends Record<string, any>>(path: string, value?: ConfigurationValue, hierarchySeparator?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, NewT>>;
addSingleValue<TKey extends string, TValue extends ConfigurationValue, TSep extends string = ':'>(path: string, value?: ConfigurationValue, hierarchySeparator?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, InflateKey<TKey, TValue, TSep>>>;
/**
* Adds a single value to the collection of data sources that will be used to build the configuration object.
* @param dataFn Function that returns the [key, value] tuple that needs to be added.
* @param hierarchySeparator Optional hierarchy separator. If not specified, colon (:) is assumed.
*/
addSingleValue<NewT extends Record<string, any>>(dataFn: () => Promise<[string, ConfigurationValue]>, hierarchySeparator?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, NewT>>;
addSingleValue<TKey extends string, TValue extends ConfigurationValue, TSep extends string = ':'>(dataFn: () => Promise<[string, ConfigurationValue]>, hierarchySeparator?: string): IEnvAwareBuilder<TEnvironments, MergeResult<T, InflateKey<TKey, TValue, TSep>>>;
/**
* Sets the data source name of the last data source added to the builder.
* @param name Name for the data source.
Expand Down
2 changes: 1 addition & 1 deletion tests/DataSource.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'chai/register-expect.js';
import { DataSource } from '../out/DataSource.js';
import { DataSource } from '../out/dataSources/DataSource.js';

describe('DataSource', () => {
it('Should make the name given during construction available through the "name" property.', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/DictionaryDataSource.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'chai/register-expect.js';
import DictionaryDataSource from '../out/DictionaryDataSource.js';
import { DictionaryDataSource } from '../out/dataSources/DictionaryDataSource.js';

describe('DictionaryDataSource', () => {
it('Should name itself as "Dictionary" upon construction.', () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/buildEnvironment.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'chai/register-expect.js';
import { buildEnvironment } from '../out/Environment.js';
import { buildEnvironment } from '../out/buildEnvironment.js';
import { forEachProperty, isConfigNode, isFunction } from '../out/helpers.js';

const testEnvNames = [
Expand Down
6 changes: 1 addition & 5 deletions tests/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'chai/register-expect.js';
import { Builder } from '../out/Builder.js';
import { Builder } from '../out/builders/Builder.js';
import * as allExports from '../out/index.js';

describe('All Exports', () => {
Expand All @@ -11,10 +11,6 @@ describe('All Exports', () => {
// Assert.
expect(allExports.EnvironmentDefinition).to.exist;
});
it('Should export the DataSource class.', () => {
// Assert.
expect(allExports.DataSource).to.exist;
});
it('Should export the entry function as default.', () => {
// Assert.
expect(allExports.default).to.exist;
Expand Down

0 comments on commit 7d02005

Please sign in to comment.