@jeengbe/config 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ast.d.mts ADDED
@@ -0,0 +1,24 @@
1
+ import { ValidationResult } from "@prisjakt/utils";
2
+ //#region src/ast.d.ts
3
+ declare const nodeType: unique symbol;
4
+ declare class EnvNode<T> {
5
+ readonly validate: (path: string, loadValue: (key: string) => string | undefined) => ValidationResult<T>;
6
+ private readonly [nodeType];
7
+ constructor(validate: (path: string, loadValue: (key: string) => string | undefined) => ValidationResult<T>);
8
+ transform<U>(transform: (value: T) => ValidationResult<U>): EnvNode<U>;
9
+ }
10
+ declare class ScalarEnvNode<T> extends EnvNode<T> {
11
+ readonly key: string;
12
+ private readonly validateValue;
13
+ constructor(key: string, validateValue: (value: string | undefined) => ValidationResult<T>);
14
+ optional(): ScalarEnvNode<T | undefined>;
15
+ transform<U>(transform: (value: T) => ValidationResult<U>): ScalarEnvNode<U>;
16
+ }
17
+ type EnvSpec = EnvNode<unknown> | {
18
+ [key: string]: EnvSpec;
19
+ };
20
+ type InferEnvSpec<T extends EnvSpec> = T extends EnvNode<infer U> ? U : T extends Record<string, EnvSpec> ? { [K in keyof T]: InferEnvSpec<T[K]>; } : never;
21
+ type Pretty<T> = T extends (infer U extends object) ? { [K in keyof U]: Pretty<U[K]>; } : T;
22
+ //#endregion
23
+ export { EnvNode, EnvSpec, InferEnvSpec, Pretty, ScalarEnvNode };
24
+ //# sourceMappingURL=ast.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.d.mts","names":[],"sources":["../src/ast.ts"],"mappings":";;cAEM;cAEO,QAAQ;WAIR,WACP,cACA,YAAY,uCACT,iBAAiB;oBANE;EAE1B,YACW,WACP,cACA,YAAY,uCACT,iBAAiB;EAGxB,UAAU,GAAG,YAAY,OAAO,MAAM,iBAAiB,KAAK,QAAQ;;cAOzD,cAAc,WAAW,QAAQ;WAEjC;mBACQ;EAFnB,YACW,aACQ,gBACf,8BACG,iBAAiB;EASxB,YAAY,cAAc;EAM1B,UAAU,GAAG,YAAY,OAAO,MAAM,iBAAiB,KAAK,cAAc;;KAOhE,UAAU;GAAsB,cAAc;;KAE9C,aAAa,UAAU,WACjC,UAAU,cAAc,KACpB,IACA,UAAU,eAAe,cACpB,WAAW,IAAI,aAAa,EAAE;KAG7B,OAAO,KAAK,iBAAgB,uBACjC,WAAW,IAAI,OAAO,EAAE,SAC3B"}
package/dist/ast.mjs ADDED
@@ -0,0 +1,34 @@
1
+ import { Either } from "@prisjakt/utils";
2
+
3
+ //#region src/ast.ts
4
+ const nodeType = Symbol("type");
5
+ var EnvNode = class EnvNode {
6
+ validate;
7
+ constructor(validate) {
8
+ this.validate = validate;
9
+ }
10
+ transform(transform) {
11
+ return new EnvNode((path, loadValue) => this.validate(path, loadValue).flatMap(transform));
12
+ }
13
+ };
14
+ var ScalarEnvNode = class ScalarEnvNode extends EnvNode {
15
+ key;
16
+ validateValue;
17
+ constructor(key, validateValue) {
18
+ super((path, loadValue) => {
19
+ return validateValue(loadValue(key)).leftMap((errors) => errors.map((error) => `${key} (${path}): ${error}`));
20
+ });
21
+ this.key = key;
22
+ this.validateValue = validateValue;
23
+ }
24
+ optional() {
25
+ return new ScalarEnvNode(this.key, (value) => value === undefined ? Either.right(undefined) : this.validateValue(value));
26
+ }
27
+ transform(transform) {
28
+ return new ScalarEnvNode(this.key, (value) => this.validateValue(value).flatMap(transform));
29
+ }
30
+ };
31
+
32
+ //#endregion
33
+ export { EnvNode, ScalarEnvNode };
34
+ //# sourceMappingURL=ast.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.mjs","names":[],"sources":["../src/ast.ts"],"sourcesContent":["import { Either, ValidationResult } from '@prisjakt/utils';\n\nconst nodeType = Symbol('type');\n\nexport class EnvNode<T> {\n declare private readonly [nodeType]: T;\n\n constructor(\n readonly validate: (\n path: string,\n loadValue: (key: string) => string | undefined,\n ) => ValidationResult<T>,\n ) {}\n\n transform<U>(transform: (value: T) => ValidationResult<U>): EnvNode<U> {\n return new EnvNode((path, loadValue) =>\n this.validate(path, loadValue).flatMap(transform),\n );\n }\n}\n\nexport class ScalarEnvNode<T> extends EnvNode<T> {\n constructor(\n readonly key: string,\n private readonly validateValue: (\n value: string | undefined,\n ) => ValidationResult<T>,\n ) {\n super((path, loadValue) => {\n return validateValue(loadValue(key)).leftMap((errors) =>\n errors.map((error) => `${key} (${path}): ${error}`),\n );\n });\n }\n\n optional(): ScalarEnvNode<T | undefined> {\n return new ScalarEnvNode<T | undefined>(this.key, (value) =>\n value === undefined ? Either.right(undefined) : this.validateValue(value),\n );\n }\n\n transform<U>(transform: (value: T) => ValidationResult<U>): ScalarEnvNode<U> {\n return new ScalarEnvNode(this.key, (value) =>\n this.validateValue(value).flatMap(transform),\n );\n }\n}\n\nexport type EnvSpec = EnvNode<unknown> | { [key: string]: EnvSpec };\n\nexport type InferEnvSpec<T extends EnvSpec> =\n T extends EnvNode<infer U>\n ? U\n : T extends Record<string, EnvSpec>\n ? { [K in keyof T]: InferEnvSpec<T[K]> }\n : never;\n\nexport type Pretty<T> = T extends infer U extends object\n ? { [K in keyof U]: Pretty<U[K]> }\n : T;\n"],"mappings":";;;AAEA,MAAM,WAAW,OAAO,MAAM;AAE9B,IAAa,UAAb,MAAa,QAAW;CAIX;CADX,YACE,AAAS,UAIT;EAJS;CAIR;CAEH,UAAa,WAA0D;EACrE,OAAO,IAAI,SAAS,MAAM,cACxB,KAAK,SAAS,MAAM,SAAS,CAAC,CAAC,QAAQ,SAAS,CAClD;CACF;AACF;AAEA,IAAa,gBAAb,MAAa,sBAAyB,QAAW;CAEpC;CACQ;CAFnB,YACE,AAAS,KACT,AAAiB,eAGjB;EACA,OAAO,MAAM,cAAc;GACzB,OAAO,cAAc,UAAU,GAAG,CAAC,CAAC,CAAC,SAAS,WAC5C,OAAO,KAAK,UAAU,GAAG,IAAI,IAAI,KAAK,KAAK,OAAO,CACpD;EACF,CAAC;EATQ;EACQ;CASnB;CAEA,WAAyC;EACvC,OAAO,IAAI,cAA6B,KAAK,MAAM,UACjD,UAAU,YAAY,OAAO,MAAM,SAAS,IAAI,KAAK,cAAc,KAAK,CAC1E;CACF;CAEA,UAAa,WAAgE;EAC3E,OAAO,IAAI,cAAc,KAAK,MAAM,UAClC,KAAK,cAAc,KAAK,CAAC,CAAC,QAAQ,SAAS,CAC7C;CACF;AACF"}
package/dist/env.d.mts ADDED
@@ -0,0 +1,18 @@
1
+ import { EnvNode, EnvSpec, InferEnvSpec, Pretty, ScalarEnvNode } from "./ast.mjs";
2
+ import { ValidationResult } from "@prisjakt/utils";
3
+ //#region src/env.d.ts
4
+ declare const env: {
5
+ string(key: string, defaultValue?: string): ScalarEnvNode<string>;
6
+ number(key: string, defaultValue?: number): ScalarEnvNode<number>;
7
+ boolean(key: string, defaultValue?: boolean): ScalarEnvNode<boolean>;
8
+ enum<const T extends string>(key: string, values: readonly T[], defaultValue?: T): ScalarEnvNode<T>;
9
+ custom<T>(key: string, transform: (value: string) => ValidationResult<T>, defaultValue?: T): ScalarEnvNode<T>;
10
+ array<T>(itemType: ScalarEnvNode<T>, defaultValue?: readonly T[]): EnvNode<readonly T[]>;
11
+ discriminate<K extends string, V extends string, M extends Partial<Record<V, Record<string, EnvSpec>>>>(discriminatorKey: K, discriminatorValueType: ScalarEnvNode<V>, mapping: M): EnvNode<DiscriminatorResult<K, V, M>>;
12
+ load<S extends EnvSpec>(spec: S): Pretty<InferEnvSpec<S>>;
13
+ };
14
+ type DiscriminatorResult<K extends string, V extends string, M extends Partial<Record<V, Record<string, EnvSpec>>>> = Pretty<V extends unknown // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- It's used only in an intersection
15
+ ? Record<K, V> & InferEnvSpec<M[V] extends EnvSpec ? M[V] : {}> : never>;
16
+ //#endregion
17
+ export { env };
18
+ //# sourceMappingURL=env.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.mts","names":[],"sources":["../src/env.ts"],"mappings":";;;cAgBa;EACX,OAAM,aAAY,wBAA0B;EAI5C,OAAM,aAAY,wBAA0B;EAc5C,QAAO,aAAY,yBAA2B;EAa9C,WAAW,kBAAgB,aACd,iBACM,KAAG,eACL,IACd,cAAc;EAcjB,OAAO,GAAC,aACK,YACC,kBAAkB,iBAAiB,IAAE,eAClC,IACd,cAAc;EAWjB,MAAM,GAAC,UACK,cAAc,IAAE,wBACF,MACvB,iBAAiB;EAsBpB,aACE,kBACA,kBACA,UAAU,QAAQ,OAAO,GAAG,eAAe,YAAU,kBAEnC,GAAC,wBACK,cAAc,IAAE,SAC/B,IACR,QAAQ,oBAAoB,GAAG,GAAG;EAqBrC,KAAK,UAAU,SAAO,MAAQ,IAAI,OAAO,aAAa;;KAYnD,oBACH,kBACA,kBACA,UAAU,QAAQ,OAAO,GAAG,eAAe,cACzC,OAGF;GAEI,OAAO,GAAG,KAAK,aAAa,EAAE,WAAW,UAAU,EAAE"}
package/dist/env.mjs ADDED
@@ -0,0 +1,75 @@
1
+ import { EnvNode, ScalarEnvNode } from "./ast.mjs";
2
+ import { Either, arrayIncludes, combineValidationResults } from "@prisjakt/utils";
3
+
4
+ //#region src/env.ts
5
+ const env = {
6
+ string(key, defaultValue) {
7
+ return env.custom(key, (value) => Either.right(value), defaultValue);
8
+ },
9
+ number(key, defaultValue) {
10
+ return env.custom(key, (value) => {
11
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) {
12
+ return Either.right(Number(value));
13
+ }
14
+ return Either.left(["invalid number"]);
15
+ }, defaultValue);
16
+ },
17
+ boolean(key, defaultValue) {
18
+ return env.custom(key, (value) => {
19
+ if (value.toLowerCase() === "true") return Either.right(true);
20
+ if (value.toLowerCase() === "false") return Either.right(false);
21
+ return Either.left(["invalid boolean (must be 'true' or 'false')"]);
22
+ }, defaultValue);
23
+ },
24
+ enum(key, values, defaultValue) {
25
+ return env.custom(key, (value) => {
26
+ if (arrayIncludes(values, value)) return Either.right(value);
27
+ return Either.left([`invalid enum value (must be one of: ${values.map((v) => `'${v}'`).join(", ")})`]);
28
+ }, defaultValue);
29
+ },
30
+ custom(key, transform, defaultValue) {
31
+ return new ScalarEnvNode(key, (value) => {
32
+ if (value === undefined) {
33
+ if (defaultValue !== undefined) return Either.right(defaultValue);
34
+ return Either.left(["required"]);
35
+ }
36
+ return transform(value);
37
+ });
38
+ },
39
+ array(itemType, defaultValue) {
40
+ return new EnvNode((path, loadValue) => {
41
+ const value = loadValue(itemType.key);
42
+ if (value === undefined) {
43
+ if (defaultValue !== undefined) return Either.right(defaultValue);
44
+ return Either.left([`${itemType.key} (${path}): required`]);
45
+ }
46
+ if (value === "") return Either.right([]);
47
+ return combineValidationResults(...value.split(",").map((v) => v.trim() || undefined).map((v, i) => itemType.validate(`${path}.${i}`, (k) => k === itemType.key ? v : undefined)));
48
+ });
49
+ },
50
+ discriminate(discriminatorKey, discriminatorValueType, mapping) {
51
+ return new EnvNode((path, loadValue) => {
52
+ return discriminatorValueType.validate(path, loadValue).flatMap((discriminatorValue) => {
53
+ return resolveNode(path, mapping[discriminatorValue] ?? {}, loadValue).map((mappingValues) => ({
54
+ [discriminatorKey]: discriminatorValue,
55
+ ...mappingValues
56
+ }));
57
+ });
58
+ });
59
+ },
60
+ load(spec) {
61
+ return resolveNode("$", spec, (key) => process.env[key]?.trim() || undefined).getOrElse((errors) => {
62
+ throw new Error(`Failed to load config: ${errors.join(", ")}`);
63
+ });
64
+ }
65
+ };
66
+ function resolveNode(path, spec, loadValue) {
67
+ if (spec instanceof EnvNode) {
68
+ return spec.validate(path, loadValue);
69
+ }
70
+ return combineValidationResults(...Object.entries(spec).map(([key, value]) => resolveNode(`${path}.${key}`, value, loadValue).map((v) => [key, v]))).map((entries) => Object.fromEntries(entries));
71
+ }
72
+
73
+ //#endregion
74
+ export { env };
75
+ //# sourceMappingURL=env.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.mjs","names":[],"sources":["../src/env.ts"],"sourcesContent":["import type {\n ValidationResult} from '@prisjakt/utils';\nimport {\n arrayIncludes,\n combineValidationResults,\n Either\n} from '@prisjakt/utils';\nimport type {\n EnvSpec,\n InferEnvSpec,\n Pretty} from './ast.js';\nimport {\n EnvNode,\n ScalarEnvNode,\n} from './ast.js';\n\nexport const env = {\n string(key: string, defaultValue?: string): ScalarEnvNode<string> {\n return env.custom(key, (value) => Either.right(value), defaultValue);\n },\n\n number(key: string, defaultValue?: number): ScalarEnvNode<number> {\n return env.custom(\n key,\n (value) => {\n if (/^-?\\d+(?:\\.\\d+)?$/.test(value)) {\n return Either.right(Number(value));\n }\n\n return Either.left(['invalid number']);\n },\n defaultValue,\n );\n },\n\n boolean(key: string, defaultValue?: boolean): ScalarEnvNode<boolean> {\n return env.custom(\n key,\n (value) => {\n if (value.toLowerCase() === 'true') return Either.right(true);\n if (value.toLowerCase() === 'false') return Either.right(false);\n\n return Either.left([\"invalid boolean (must be 'true' or 'false')\"]);\n },\n defaultValue,\n );\n },\n\n enum<const T extends string>(\n key: string,\n values: readonly T[],\n defaultValue?: T,\n ): ScalarEnvNode<T> {\n return env.custom(\n key,\n (value) => {\n if (arrayIncludes(values, value)) return Either.right(value);\n\n return Either.left([\n `invalid enum value (must be one of: ${values.map((v) => `'${v}'`).join(', ')})`,\n ]);\n },\n defaultValue,\n );\n },\n\n custom<T>(\n key: string,\n transform: (value: string) => ValidationResult<T>,\n defaultValue?: T,\n ): ScalarEnvNode<T> {\n return new ScalarEnvNode(key, (value) => {\n if (value === undefined) {\n if (defaultValue !== undefined) return Either.right(defaultValue);\n return Either.left(['required']);\n }\n\n return transform(value);\n });\n },\n\n array<T>(\n itemType: ScalarEnvNode<T>,\n defaultValue?: readonly T[],\n ): EnvNode<readonly T[]> {\n return new EnvNode((path, loadValue) => {\n const value = loadValue(itemType.key);\n if (value === undefined) {\n if (defaultValue !== undefined) return Either.right(defaultValue);\n return Either.left([`${itemType.key} (${path}): required`]);\n }\n if (value === '') return Either.right([]);\n\n return combineValidationResults(\n ...value\n .split(',')\n .map((v) => v.trim() || undefined)\n .map((v, i) =>\n itemType.validate(`${path}.${i}`, (k) =>\n k === itemType.key ? v : undefined,\n ),\n ),\n );\n });\n },\n\n discriminate<\n K extends string,\n V extends string,\n M extends Partial<Record<V, Record<string, EnvSpec>>>,\n >(\n discriminatorKey: K,\n discriminatorValueType: ScalarEnvNode<V>,\n mapping: M,\n ): EnvNode<DiscriminatorResult<K, V, M>> {\n return new EnvNode((path, loadValue) => {\n return discriminatorValueType\n .validate(path, loadValue)\n .flatMap((discriminatorValue) => {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- This is the type that we map over\n return resolveNode<NonNullable<M[keyof M]> | {}>(\n path,\n mapping[discriminatorValue] ?? {},\n loadValue,\n ).map(\n (mappingValues) =>\n ({\n [discriminatorKey]: discriminatorValue,\n ...mappingValues,\n }) as DiscriminatorResult<K, V, M>,\n );\n });\n });\n },\n\n load<S extends EnvSpec>(spec: S): Pretty<InferEnvSpec<S>> {\n return resolveNode(\n '$',\n spec,\n // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Empty string -> undefined\n (key) => process.env[key]?.trim() || undefined,\n ).getOrElse((errors) => {\n throw new Error(`Failed to load config: ${errors.join(', ')}`);\n });\n },\n};\n\ntype DiscriminatorResult<\n K extends string,\n V extends string,\n M extends Partial<Record<V, Record<string, EnvSpec>>>,\n> = Pretty<\n // This \"redundant\" condition is necessary to make sure that 'DiscriminatorResult' distributes over\n // the union type V\n V extends unknown\n ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- It's used only in an intersection\n Record<K, V> & InferEnvSpec<M[V] extends EnvSpec ? M[V] : {}>\n : never\n>;\n\nfunction resolveNode<S extends EnvSpec>(\n path: string,\n spec: S,\n loadValue: (key: string) => string | undefined,\n): ValidationResult<Pretty<InferEnvSpec<S>>> {\n if (spec instanceof EnvNode) {\n return (spec as EnvNode<Pretty<InferEnvSpec<S>>>).validate(path, loadValue);\n }\n\n return combineValidationResults(\n ...Object.entries(spec).map(([key, value]) =>\n resolveNode(`${path}.${key}`, value, loadValue).map(\n (v) => [key, v] as const,\n ),\n ),\n ).map((entries) => Object.fromEntries(entries) as Pretty<InferEnvSpec<S>>);\n}\n"],"mappings":";;;;AAgBA,MAAa,MAAM;CACjB,OAAO,KAAa,cAA8C;EAChE,OAAO,IAAI,OAAO,MAAM,UAAU,OAAO,MAAM,KAAK,GAAG,YAAY;CACrE;CAEA,OAAO,KAAa,cAA8C;EAChE,OAAO,IAAI,OACT,MACC,UAAU;GACT,IAAI,oBAAoB,KAAK,KAAK,GAAG;IACnC,OAAO,OAAO,MAAM,OAAO,KAAK,CAAC;GACnC;GAEA,OAAO,OAAO,KAAK,CAAC,gBAAgB,CAAC;EACvC,GACA,YACF;CACF;CAEA,QAAQ,KAAa,cAAgD;EACnE,OAAO,IAAI,OACT,MACC,UAAU;GACT,IAAI,MAAM,YAAY,MAAM,QAAQ,OAAO,OAAO,MAAM,IAAI;GAC5D,IAAI,MAAM,YAAY,MAAM,SAAS,OAAO,OAAO,MAAM,KAAK;GAE9D,OAAO,OAAO,KAAK,CAAC,6CAA6C,CAAC;EACpE,GACA,YACF;CACF;CAEA,KACE,KACA,QACA,cACkB;EAClB,OAAO,IAAI,OACT,MACC,UAAU;GACT,IAAI,cAAc,QAAQ,KAAK,GAAG,OAAO,OAAO,MAAM,KAAK;GAE3D,OAAO,OAAO,KAAK,CACjB,uCAAuC,OAAO,KAAK,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,EAChF,CAAC;EACH,GACA,YACF;CACF;CAEA,OACE,KACA,WACA,cACkB;EAClB,OAAO,IAAI,cAAc,MAAM,UAAU;GACvC,IAAI,UAAU,WAAW;IACvB,IAAI,iBAAiB,WAAW,OAAO,OAAO,MAAM,YAAY;IAChE,OAAO,OAAO,KAAK,CAAC,UAAU,CAAC;GACjC;GAEA,OAAO,UAAU,KAAK;EACxB,CAAC;CACH;CAEA,MACE,UACA,cACuB;EACvB,OAAO,IAAI,SAAS,MAAM,cAAc;GACtC,MAAM,QAAQ,UAAU,SAAS,GAAG;GACpC,IAAI,UAAU,WAAW;IACvB,IAAI,iBAAiB,WAAW,OAAO,OAAO,MAAM,YAAY;IAChE,OAAO,OAAO,KAAK,CAAC,GAAG,SAAS,IAAI,IAAI,KAAK,YAAY,CAAC;GAC5D;GACA,IAAI,UAAU,IAAI,OAAO,OAAO,MAAM,CAAC,CAAC;GAExC,OAAO,yBACL,GAAG,MACA,MAAM,GAAG,CAAC,CACV,KAAK,MAAM,EAAE,KAAK,KAAK,SAAS,CAAC,CACjC,KAAK,GAAG,MACP,SAAS,SAAS,GAAG,KAAK,GAAG,MAAM,MACjC,MAAM,SAAS,MAAM,IAAI,SAC3B,CACF,CACJ;EACF,CAAC;CACH;CAEA,aAKE,kBACA,wBACA,SACuC;EACvC,OAAO,IAAI,SAAS,MAAM,cAAc;GACtC,OAAO,uBACJ,SAAS,MAAM,SAAS,CAAC,CACzB,SAAS,uBAAuB;IAE/B,OAAO,YACL,MACA,QAAQ,uBAAuB,CAAC,GAChC,SACF,CAAC,CAAC,KACC,mBACE;MACE,mBAAmB;KACpB,GAAG;IACL,EACJ;GACF,CAAC;EACL,CAAC;CACH;CAEA,KAAwB,MAAkC;EACxD,OAAO,YACL,KACA,OAEC,QAAQ,QAAQ,IAAI,IAAI,EAAE,KAAK,KAAK,SACvC,CAAC,CAAC,WAAW,WAAW;GACtB,MAAM,IAAI,MAAM,0BAA0B,OAAO,KAAK,IAAI,GAAG;EAC/D,CAAC;CACH;AACF;AAeA,SAAS,YACP,MACA,MACA,WAC2C;CAC3C,IAAI,gBAAgB,SAAS;EAC3B,OAAQ,KAA0C,SAAS,MAAM,SAAS;CAC5E;CAEA,OAAO,yBACL,GAAG,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WACjC,YAAY,GAAG,KAAK,GAAG,OAAO,OAAO,SAAS,CAAC,CAAC,KAC7C,MAAM,CAAC,KAAK,CAAC,CAChB,CACF,CACF,CAAC,CAAC,KAAK,YAAY,OAAO,YAAY,OAAO,CAA4B;AAC3E"}
@@ -0,0 +1,11 @@
1
+ import { EnvNode, EnvSpec, InferEnvSpec } from "./ast.mjs";
2
+ //#region src/if-enabled.d.ts
3
+ type IfEnabled<T> = ({
4
+ enabled: true;
5
+ } & T) | {
6
+ enabled: false;
7
+ };
8
+ declare function ifEnabled<T extends Record<string, EnvSpec>>(envVar: string, config: T, defaultEnabled?: boolean): EnvNode<IfEnabled<InferEnvSpec<T>>>;
9
+ //#endregion
10
+ export { ifEnabled };
11
+ //# sourceMappingURL=if-enabled.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"if-enabled.d.mts","names":[],"sources":["../src/if-enabled.ts"],"mappings":";;KAKK,UAAU;EAET;IACE;EACF;;iBAEU,UAAU,UAAU,eAAe,UACjD,gBACA,QAAQ,GACR,2BACC,QAAQ,UAAU,aAAa"}
@@ -0,0 +1,19 @@
1
+ import { env } from "./env.mjs";
2
+ import { Either } from "@prisjakt/utils";
3
+
4
+ //#region src/if-enabled.ts
5
+ function ifEnabled(envVar, config, defaultEnabled = false) {
6
+ return env.discriminate("enabled", env.boolean(envVar, defaultEnabled).transform((v) => Either.right(v ? "enabled" : "disabled")), { enabled: config }).transform((value) => {
7
+ if (value.enabled === "enabled") {
8
+ return Either.right({
9
+ ...value,
10
+ enabled: true
11
+ });
12
+ }
13
+ return Either.right({ enabled: false });
14
+ });
15
+ }
16
+
17
+ //#endregion
18
+ export { ifEnabled };
19
+ //# sourceMappingURL=if-enabled.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"if-enabled.mjs","names":[],"sources":["../src/if-enabled.ts"],"sourcesContent":["import type { ValidationResult } from '@prisjakt/utils';\nimport { Either } from '@prisjakt/utils';\nimport type { EnvNode, EnvSpec, InferEnvSpec } from './ast.js';\nimport { env } from './env.js';\n\ntype IfEnabled<T> =\n | ({\n enabled: true;\n } & T)\n | { enabled: false };\n\nexport function ifEnabled<T extends Record<string, EnvSpec>>(\n envVar: string,\n config: T,\n defaultEnabled = false,\n): EnvNode<IfEnabled<InferEnvSpec<T>>> {\n // Since discriminate only works with string values, we need to bridge 'true' -> 'enabled' -> true\n return env\n .discriminate(\n 'enabled',\n env\n .boolean(envVar, defaultEnabled)\n .transform((v): ValidationResult<'enabled' | 'disabled'> =>\n Either.right(v ? 'enabled' : 'disabled'),\n ),\n {\n enabled: config,\n },\n )\n .transform((value): ValidationResult<IfEnabled<InferEnvSpec<T>>> => {\n if (value.enabled === 'enabled') {\n return Either.right({\n ...value,\n enabled: true,\n });\n }\n\n return Either.right({\n enabled: false,\n });\n });\n}\n"],"mappings":";;;;AAWA,SAAgB,UACd,QACA,QACA,iBAAiB,OACoB;CAErC,OAAO,IACJ,aACC,WACA,IACG,QAAQ,QAAQ,cAAc,CAAC,CAC/B,WAAW,MACV,OAAO,MAAM,IAAI,YAAY,UAAU,CACzC,GACF,EACE,SAAS,OACX,CACF,CAAC,CACA,WAAW,UAAwD;EAClE,IAAI,MAAM,YAAY,WAAW;GAC/B,OAAO,OAAO,MAAM;IAClB,GAAG;IACH,SAAS;GACX,CAAC;EACH;EAEA,OAAO,OAAO,MAAM,EAClB,SAAS,MACX,CAAC;CACH,CAAC;AACL"}
@@ -0,0 +1,4 @@
1
+ import { EnvNode, EnvSpec, InferEnvSpec } from "./ast.mjs";
2
+ import { env } from "./env.mjs";
3
+ import { ifEnabled } from "./if-enabled.mjs";
4
+ export { type EnvNode, type EnvSpec, type InferEnvSpec, env, ifEnabled };
package/dist/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import { env } from "./env.mjs";
2
+ import { ifEnabled } from "./if-enabled.mjs";
3
+
4
+ export { env, ifEnabled };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@jeengbe/config",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "files": [
6
+ "dist",
7
+ "src",
8
+ "!src/**/*.spec.ts"
9
+ ],
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.mts",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@jeengbe/prelude": "0.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "oxfmt": "^0.60.0",
26
+ "oxlint": "^1.75.0",
27
+ "tsdown": "^0.22.14",
28
+ "typescript": "^7.0.2",
29
+ "vitest": "^4.1.10",
30
+ "@jeengbe/scaffolding": "0.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "lint": "oxlint --fix . && oxfmt --write .",
35
+ "test": "vitest",
36
+ "typecheck": "tsc"
37
+ }
38
+ }
package/src/ast.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { ValidationResult } from './validation.js';
2
+ import { Either } from '@jeengbe/prelude';
3
+
4
+ const nodeType = Symbol('type');
5
+
6
+ export class EnvNode<T> {
7
+ declare private readonly [nodeType]: T;
8
+
9
+ constructor(
10
+ readonly validate: (
11
+ path: string,
12
+ loadValue: (key: string) => string | undefined,
13
+ ) => ValidationResult<T>,
14
+ ) {}
15
+
16
+ transform<U>(transform: (value: T) => ValidationResult<U>): EnvNode<U> {
17
+ return new EnvNode((path, loadValue) => this.validate(path, loadValue).flatMap(transform));
18
+ }
19
+ }
20
+
21
+ export class ScalarEnvNode<T> extends EnvNode<T> {
22
+ constructor(
23
+ readonly key: string,
24
+ private readonly validateValue: (value: string | undefined) => ValidationResult<T>,
25
+ ) {
26
+ super((path, loadValue) => {
27
+ return validateValue(loadValue(key)).leftMap((errors) =>
28
+ errors.map((error) => `${key} (${path}): ${error}`),
29
+ );
30
+ });
31
+ }
32
+
33
+ optional(): ScalarEnvNode<T | undefined> {
34
+ return new ScalarEnvNode<T | undefined>(this.key, (value) =>
35
+ value === undefined ? Either.right(undefined) : this.validateValue(value),
36
+ );
37
+ }
38
+
39
+ override transform<U>(transform: (value: T) => ValidationResult<U>): ScalarEnvNode<U> {
40
+ return new ScalarEnvNode(this.key, (value) => this.validateValue(value).flatMap(transform));
41
+ }
42
+ }
43
+
44
+ export type EnvSpec = EnvNode<unknown> | { [key: string]: EnvSpec };
45
+
46
+ export type InferEnvSpec<T extends EnvSpec> =
47
+ T extends EnvNode<infer U>
48
+ ? U
49
+ : T extends Record<string, EnvSpec>
50
+ ? { [K in keyof T]: InferEnvSpec<T[K]> }
51
+ : never;
52
+
53
+ export type Pretty<T> = T extends infer U extends object ? { [K in keyof U]: Pretty<U[K]> } : T;
package/src/env.ts ADDED
@@ -0,0 +1,151 @@
1
+ import type { EnvSpec, InferEnvSpec, Pretty } from './ast.js';
2
+ import { EnvNode, ScalarEnvNode } from './ast.js';
3
+ import type { ValidationResult } from './validation.js';
4
+ import { arrayIncludes, combineValidationResults } from './validation.js';
5
+ import { Either } from '@jeengbe/prelude';
6
+
7
+ export const env = {
8
+ string(key: string, defaultValue?: string): ScalarEnvNode<string> {
9
+ return env.custom(key, (value) => Either.right(value), defaultValue);
10
+ },
11
+
12
+ number(key: string, defaultValue?: number): ScalarEnvNode<number> {
13
+ return env.custom(
14
+ key,
15
+ (value) => {
16
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) {
17
+ return Either.right(Number(value));
18
+ }
19
+
20
+ return Either.left(['invalid number']);
21
+ },
22
+ defaultValue,
23
+ );
24
+ },
25
+
26
+ boolean(key: string, defaultValue?: boolean): ScalarEnvNode<boolean> {
27
+ return env.custom(
28
+ key,
29
+ (value) => {
30
+ if (value.toLowerCase() === 'true') return Either.right(true);
31
+ if (value.toLowerCase() === 'false') return Either.right(false);
32
+
33
+ return Either.left(["invalid boolean (must be 'true' or 'false')"]);
34
+ },
35
+ defaultValue,
36
+ );
37
+ },
38
+
39
+ enum<const T extends string>(
40
+ key: string,
41
+ values: readonly T[],
42
+ defaultValue?: T,
43
+ ): ScalarEnvNode<T> {
44
+ return env.custom(
45
+ key,
46
+ (value) => {
47
+ if (arrayIncludes(values, value)) return Either.right(value);
48
+
49
+ return Either.left([
50
+ `invalid enum value (must be one of: ${values.map((v) => `'${v}'`).join(', ')})`,
51
+ ]);
52
+ },
53
+ defaultValue,
54
+ );
55
+ },
56
+
57
+ custom<T>(
58
+ key: string,
59
+ transform: (value: string) => ValidationResult<T>,
60
+ defaultValue?: T,
61
+ ): ScalarEnvNode<T> {
62
+ return new ScalarEnvNode(key, (value) => {
63
+ if (value === undefined) {
64
+ if (defaultValue !== undefined) return Either.right(defaultValue);
65
+ return Either.left(['required']);
66
+ }
67
+
68
+ return transform(value);
69
+ });
70
+ },
71
+
72
+ array<T>(itemType: ScalarEnvNode<T>, defaultValue?: readonly T[]): EnvNode<readonly T[]> {
73
+ return new EnvNode((path, loadValue) => {
74
+ const value = loadValue(itemType.key);
75
+ if (value === undefined) {
76
+ if (defaultValue !== undefined) return Either.right(defaultValue);
77
+ return Either.left([`${itemType.key} (${path}): required`]);
78
+ }
79
+ if (value === '') return Either.right([]);
80
+
81
+ return combineValidationResults(
82
+ ...value
83
+ .split(',')
84
+ .map((v) => v.trim() || undefined)
85
+ .map((v, i) =>
86
+ itemType.validate(`${path}.${i}`, (k) => (k === itemType.key ? v : undefined)),
87
+ ),
88
+ );
89
+ });
90
+ },
91
+
92
+ discriminate<
93
+ K extends string,
94
+ V extends string,
95
+ M extends Partial<Record<V, Record<string, EnvSpec>>>,
96
+ >(
97
+ discriminatorKey: K,
98
+ discriminatorValueType: ScalarEnvNode<V>,
99
+ mapping: M,
100
+ ): EnvNode<DiscriminatorResult<K, V, M>> {
101
+ return new EnvNode((path, loadValue) => {
102
+ return discriminatorValueType.validate(path, loadValue).flatMap((discriminatorValue) => {
103
+ return resolveNode<NonNullable<M[keyof M]> | {}>(
104
+ path,
105
+ mapping[discriminatorValue] ?? {},
106
+ loadValue,
107
+ ).map(
108
+ (mappingValues) =>
109
+ ({
110
+ [discriminatorKey]: discriminatorValue,
111
+ ...mappingValues,
112
+ }) as DiscriminatorResult<K, V, M>,
113
+ );
114
+ });
115
+ });
116
+ },
117
+
118
+ load<S extends EnvSpec>(spec: S): Pretty<InferEnvSpec<S>> {
119
+ return resolveNode('$', spec, (key) => process.env[key]?.trim() || undefined).getOrElse(
120
+ (errors) => {
121
+ throw new Error(`Failed to load config: ${errors.join(', ')}`);
122
+ },
123
+ );
124
+ },
125
+ };
126
+
127
+ type DiscriminatorResult<
128
+ K extends string,
129
+ V extends string,
130
+ M extends Partial<Record<V, Record<string, EnvSpec>>>,
131
+ > = Pretty<
132
+ // This "redundant" condition is necessary to make sure that 'DiscriminatorResult' distributes over
133
+ // the union type V
134
+ V extends unknown ? Record<K, V> & InferEnvSpec<M[V] extends EnvSpec ? M[V] : {}> : never
135
+ >;
136
+
137
+ function resolveNode<S extends EnvSpec>(
138
+ path: string,
139
+ spec: S,
140
+ loadValue: (key: string) => string | undefined,
141
+ ): ValidationResult<Pretty<InferEnvSpec<S>>> {
142
+ if (spec instanceof EnvNode) {
143
+ return (spec as EnvNode<Pretty<InferEnvSpec<S>>>).validate(path, loadValue);
144
+ }
145
+
146
+ return combineValidationResults(
147
+ ...Object.entries(spec).map(([key, value]) =>
148
+ resolveNode(`${path}.${key}`, value, loadValue).map((v) => [key, v] as const),
149
+ ),
150
+ ).map((entries) => Object.fromEntries(entries) as Pretty<InferEnvSpec<S>>);
151
+ }
@@ -0,0 +1,42 @@
1
+ import type { EnvNode, EnvSpec, InferEnvSpec } from './ast.js';
2
+ import { env } from './env.js';
3
+ import type { ValidationResult } from './validation.js';
4
+ import { Either } from '@jeengbe/prelude';
5
+
6
+ type IfEnabled<T> =
7
+ | ({
8
+ enabled: true;
9
+ } & T)
10
+ | { enabled: false };
11
+
12
+ export function ifEnabled<T extends Record<string, EnvSpec>>(
13
+ envVar: string,
14
+ config: T,
15
+ defaultEnabled = false,
16
+ ): EnvNode<IfEnabled<InferEnvSpec<T>>> {
17
+ // Since discriminate only works with string values, we need to bridge 'true' -> 'enabled' -> true
18
+ return env
19
+ .discriminate(
20
+ 'enabled',
21
+ env
22
+ .boolean(envVar, defaultEnabled)
23
+ .transform(
24
+ (v): ValidationResult<'enabled' | 'disabled'> => Either.right(v ? 'enabled' : 'disabled'),
25
+ ),
26
+ {
27
+ enabled: config,
28
+ },
29
+ )
30
+ .transform((value): ValidationResult<IfEnabled<InferEnvSpec<T>>> => {
31
+ if (value.enabled === 'enabled') {
32
+ return Either.right({
33
+ ...value,
34
+ enabled: true,
35
+ });
36
+ }
37
+
38
+ return Either.right({
39
+ enabled: false,
40
+ });
41
+ });
42
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type { EnvNode, EnvSpec, InferEnvSpec } from './ast.js';
2
+ export { env } from './env.js';
3
+ export { ifEnabled } from './if-enabled.js';
@@ -0,0 +1,21 @@
1
+ import { Either } from '@jeengbe/prelude';
2
+
3
+ export type ValidationResult<T> = Either<readonly string[], T>;
4
+
5
+ type CombineValidationResult<T extends readonly ValidationResult<unknown>[]> = ValidationResult<{
6
+ [K in keyof T]: T[K] extends ValidationResult<infer U> ? U : never;
7
+ }>;
8
+
9
+ export function combineValidationResults<const U extends readonly ValidationResult<unknown>[]>(
10
+ ...results: U
11
+ ): CombineValidationResult<U> {
12
+ const errors = results.filter((r) => r.isLeft()).flatMap((r) => r.getLeft());
13
+
14
+ return errors.length
15
+ ? Either.left(errors)
16
+ : (Either.right(results.map((r) => r.get())) as CombineValidationResult<U>);
17
+ }
18
+
19
+ export function arrayIncludes<const T extends string>(arr: readonly T[], val: unknown): val is T {
20
+ return arr.includes(val as T);
21
+ }