@jeengbe/config 0.0.9 → 1.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/README.md CHANGED
@@ -60,15 +60,15 @@ const config = env.load({
60
60
 
61
61
  Without a `defaultValue`, all of these are required and fail validation when the variable is missing.
62
62
 
63
- ### Custom scalars (`env.custom`)
63
+ ### Custom scalars (`env.scalar`)
64
64
 
65
- For anything else, write your own parser with `env.custom`. It returns a `ValidationResult<T>` (an `Either<readonly string[], T>` from `@jeengbe/prelude`):
65
+ For anything else, write your own parser with `env.scalar`. It returns a `ValidationResult<T>` (an `Either<readonly string[], T>` from `@jeengbe/prelude`):
66
66
 
67
67
  ```ts
68
68
  import { env } from '@jeengbe/config';
69
69
  import { Either } from '@jeengbe/prelude';
70
70
 
71
- const apiUrl = env.custom('API_URL', (value) => {
71
+ const apiUrl = env.scalar('API_URL', (value) => {
72
72
  try {
73
73
  return Either.right(new URL(value));
74
74
  } catch {
package/dist/ast.d.mts CHANGED
@@ -1,24 +1,124 @@
1
- import { ValidationResult } from "./validation.mjs";
1
+ import { ValidationFailure, ValidationResult } from "./validation.mjs";
2
+ import { Either } from "@jeengbe/prelude";
2
3
  //#region src/ast.d.ts
3
4
  declare const nodeType: unique symbol;
5
+ /**
6
+ * A node in a config schema that validates a value out of the loaded environment.
7
+ *
8
+ * Do not instantiate this class directly; use the {@link Env} interface instead.
9
+ */
4
10
  declare class EnvNode<T> {
5
- readonly validate: (path: string, loadValue: (key: string) => string | undefined) => ValidationResult<T>;
6
11
  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>;
12
+ readonly validate: (loadValue: (key: string) => string | undefined, path: string) => ValidationResult<T>;
13
+ constructor(validate: (loadValue: (key: string) => string | undefined, path: string) => ValidationResultOrEither<T>);
14
+ /**
15
+ * Transforms the validated value of this node into a new value, or fails validation.
16
+ *
17
+ * @example
18
+ *
19
+ * ```ts
20
+ * const res = env.load(
21
+ * // ^? number
22
+ * env.number('PORT').transform((port, path) =>
23
+ * port > 0
24
+ * ? ValidationResult.success({ value: port, defaulted: [] })
25
+ * : ValidationResult.fail({
26
+ * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],
27
+ * }),
28
+ * ),
29
+ * );
30
+ * ```
31
+ */
32
+ transform<U>(transform: (value: T, path: string) => ValidationResultOrEither<U>): EnvNode<U>;
9
33
  }
34
+ /**
35
+ * An {@link EnvNode} backed by a single environment variable.
36
+ *
37
+ * Do not instantiate this class directly; use the {@link Env} interface instead.
38
+ */
10
39
  declare class ScalarEnvNode<T> extends EnvNode<T> {
11
40
  readonly key: string;
12
41
  private readonly validateValue;
13
- constructor(key: string, validateValue: (value: string | undefined) => ValidationResult<T>);
42
+ constructor(key: string, validateValue: (value: string | undefined, path: string) => ScalarValidationResultOrEither<T>);
43
+ /**
44
+ * Returns a new ScalarEnvNode that resolves to undefined instead of failing validation when the environment variable is not set.
45
+ *
46
+ * @example
47
+ *
48
+ * ```ts
49
+ * const res = env.load(
50
+ * // ^? number | undefined
51
+ * env.number('PORT').optional(),
52
+ * );
53
+ *
54
+ * // PORT=3000 -> 3000
55
+ * // PORT= -> error
56
+ * // (not set) -> undefined
57
+ * ```
58
+ */
14
59
  optional(): ScalarEnvNode<T | undefined>;
15
- transform<U>(transform: (value: T) => ValidationResult<U>): ScalarEnvNode<U>;
60
+ /**
61
+ * Transforms the validated value of this node into a new value, or fails validation.
62
+ *
63
+ * As a shorthand for the common case of a single error message, `transform` may return a plain
64
+ * `Either<string, U>` instead of a full `ValidationResult<U>`. The error string is automatically
65
+ * wrapped into a {@link ValidationError} using this node's key, its path, and the value that failed to
66
+ * transform.
67
+ *
68
+ * @example
69
+ *
70
+ * ```ts
71
+ * const res = env.load(
72
+ * // ^? number
73
+ * env.number('PORT').transform((port) =>
74
+ * port > 0 ? Either.right(port) : Either.left('must be positive'),
75
+ * ),
76
+ * );
77
+ * ```
78
+ *
79
+ * @example
80
+ *
81
+ * ```ts
82
+ * const res = env.load(
83
+ * // ^? number
84
+ * env.number('PORT').transform((port, path) =>
85
+ * port > 0
86
+ * ? ValidationResult.success({ value: port, defaulted: [] })
87
+ * : ValidationResult.fail({
88
+ * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],
89
+ * }),
90
+ * ),
91
+ * );
92
+ * ```
93
+ */
94
+ transform<U>(transform: (value: T, path: string) => ScalarValidationResultOrEither<U>): ScalarEnvNode<U>;
16
95
  }
96
+ type ValidationResultOrEither<T> = ValidationResult<T> | Either<ValidationFailure, T>;
97
+ type ScalarValidationResultOrEither<T> = ValidationResultOrEither<T> | Either<string, T>;
98
+ /**
99
+ * Describes the shape of a config schema: either a single EnvNode, or a nested object of them.
100
+ */
17
101
  type EnvSpec = EnvNode<unknown> | {
18
102
  [key: string]: EnvSpec;
19
103
  };
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;
104
+ /**
105
+ * Infers the resulting value type produced by loading an EnvSpec.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * export function createModuleConfig() {
110
+ * return {
111
+ * url: env.string('URL'),
112
+ * port: env.number('PORT').optional(),
113
+ * };
114
+ * }
115
+ *
116
+ * export type ModuleConfig = ParseEnv<ReturnType<typeof createModuleConfig>>;
117
+ * // ^? { url: string; port?: number | undefined; }
118
+ * ```
119
+ */
120
+ type ParseEnv<T extends EnvSpec> = T extends EnvNode<infer U> ? U : T extends Record<string, EnvSpec> ? { [K in keyof T]: ParseEnv<T[K]>; } : never;
21
121
  type Pretty<T> = T extends (infer U extends object) ? { [K in keyof U]: Pretty<U[K]>; } : T;
22
122
  //#endregion
23
- export { EnvNode, EnvSpec, InferEnvSpec, Pretty, ScalarEnvNode };
123
+ export { EnvNode, EnvSpec, ParseEnv, Pretty, ScalarEnvNode, ScalarValidationResultOrEither, ValidationResultOrEither };
24
124
  //# sourceMappingURL=ast.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ast.d.mts","names":[],"sources":["../src/ast.ts"],"mappings":";;cAGM;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;;cAKzD,cAAc,WAAW,QAAQ;WAEjC;mBACQ;EAFnB,YACW,aACQ,gBAAgB,8BAA8B,iBAAiB;EASlF,YAAY,cAAc;EAMjB,UAAU,GAAG,YAAY,OAAO,MAAM,iBAAiB,KAAK,cAAc;;KAKzE,UAAU;GAAsB,cAAc;;KAE9C,aAAa,UAAU,WACjC,UAAU,cAAc,KACpB,IACA,UAAU,eAAe,cACpB,WAAW,IAAI,aAAa,EAAE;KAG7B,OAAO,KAAK,iBAAgB,uBAAsB,WAAW,IAAI,OAAO,EAAE,SAAQ"}
1
+ {"version":3,"file":"ast.d.mts","names":[],"sources":["../src/ast.ts"],"mappings":";;;cAQM;;;;;;cAOO,QAAQ;oBACO;WACjB,WACP,YAAY,oCACZ,iBACG,iBAAiB;EAEtB,YACE,WACE,YAAY,oCACZ,iBACG,yBAAyB;;;;;;;;;;;;;;;;;;;EAuBhC,UAAU,GAAG,YAAY,OAAO,GAAG,iBAAiB,yBAAyB,KAAK,QAAQ;;;;;;;cAc/E,cAAc,WAAW,QAAQ;WAEjC;mBACQ;EAFnB,YACW,aACQ,gBACf,2BACA,iBACG,+BAA+B;;;;;;;;;;;;;;;;;EAyBtC,YAAY,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmDjB,UAAU,GACjB,YAAY,OAAO,GAAG,iBAAiB,+BAA+B,KACrE,cAAc;;KAcP,yBAAyB,KAAK,iBAAiB,KAAK,OAAO,mBAAmB;KAmB9E,+BAA+B,KAAK,yBAAyB,KAAK,eAAe;;;;KAuCjF,UAAU;GAAsB,cAAc;;;;;;;;;;;;;;;;;;KAkB9C,SAAS,UAAU,WAC7B,UAAU,cAAc,KACpB,IACA,UAAU,eAAe,cACpB,WAAW,IAAI,SAAS,EAAE;KAGzB,OAAO,KAAK,iBAAgB,uBAAsB,WAAW,IAAI,OAAO,EAAE,SAAQ"}
package/dist/ast.mjs CHANGED
@@ -1,32 +1,138 @@
1
- import { Either } from "@jeengbe/prelude";
1
+ import { ValidationResult } from "./validation.mjs";
2
+ import { EitherBase } from "@jeengbe/prelude";
2
3
 
3
4
  //#region src/ast.ts
5
+ /**
6
+ * A node in a config schema that validates a value out of the loaded environment.
7
+ *
8
+ * Do not instantiate this class directly; use the {@link Env} interface instead.
9
+ */
4
10
  var EnvNode = class EnvNode {
5
11
  validate;
6
12
  constructor(validate) {
7
- this.validate = validate;
13
+ this.validate = (loadValue, path) => recoverValidationResultOrEither(validate(loadValue, path));
8
14
  }
15
+ /**
16
+ * Transforms the validated value of this node into a new value, or fails validation.
17
+ *
18
+ * @example
19
+ *
20
+ * ```ts
21
+ * const res = env.load(
22
+ * // ^? number
23
+ * env.number('PORT').transform((port, path) =>
24
+ * port > 0
25
+ * ? ValidationResult.success({ value: port, defaulted: [] })
26
+ * : ValidationResult.fail({
27
+ * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],
28
+ * }),
29
+ * ),
30
+ * );
31
+ * ```
32
+ */
9
33
  transform(transform) {
10
- return new EnvNode((path, loadValue) => this.validate(path, loadValue).flatMap(transform));
34
+ return new EnvNode((loadValue, path) => this.validate(loadValue, path).flatMap((value) => recoverValidationResultOrEither(transform(value, path))));
11
35
  }
12
36
  };
37
+ /**
38
+ * An {@link EnvNode} backed by a single environment variable.
39
+ *
40
+ * Do not instantiate this class directly; use the {@link Env} interface instead.
41
+ */
13
42
  var ScalarEnvNode = class ScalarEnvNode extends EnvNode {
14
43
  key;
15
44
  validateValue;
16
45
  constructor(key, validateValue) {
17
- super((path, loadValue) => {
18
- return validateValue(loadValue(key)).leftMap((errors) => errors.map((error) => `${key} (${path}): ${error}`));
46
+ super((loadValue, path) => {
47
+ const value = loadValue(key);
48
+ return recoverScalarValidationResultOrEither(validateValue(value, path), path, key, value);
19
49
  });
20
50
  this.key = key;
21
51
  this.validateValue = validateValue;
22
52
  }
53
+ /**
54
+ * Returns a new ScalarEnvNode that resolves to undefined instead of failing validation when the environment variable is not set.
55
+ *
56
+ * @example
57
+ *
58
+ * ```ts
59
+ * const res = env.load(
60
+ * // ^? number | undefined
61
+ * env.number('PORT').optional(),
62
+ * );
63
+ *
64
+ * // PORT=3000 -> 3000
65
+ * // PORT= -> error
66
+ * // (not set) -> undefined
67
+ * ```
68
+ */
23
69
  optional() {
24
- return new ScalarEnvNode(this.key, (value) => value === void 0 ? Either.right(void 0) : this.validateValue(value));
70
+ return new ScalarEnvNode(this.key, (value, path) => value === void 0 ? ValidationResult.success({
71
+ value: void 0,
72
+ defaulted: [{
73
+ path,
74
+ key: this.key,
75
+ defaultValue: void 0
76
+ }]
77
+ }) : this.validateValue(value, path));
25
78
  }
79
+ /**
80
+ * Transforms the validated value of this node into a new value, or fails validation.
81
+ *
82
+ * As a shorthand for the common case of a single error message, `transform` may return a plain
83
+ * `Either<string, U>` instead of a full `ValidationResult<U>`. The error string is automatically
84
+ * wrapped into a {@link ValidationError} using this node's key, its path, and the value that failed to
85
+ * transform.
86
+ *
87
+ * @example
88
+ *
89
+ * ```ts
90
+ * const res = env.load(
91
+ * // ^? number
92
+ * env.number('PORT').transform((port) =>
93
+ * port > 0 ? Either.right(port) : Either.left('must be positive'),
94
+ * ),
95
+ * );
96
+ * ```
97
+ *
98
+ * @example
99
+ *
100
+ * ```ts
101
+ * const res = env.load(
102
+ * // ^? number
103
+ * env.number('PORT').transform((port, path) =>
104
+ * port > 0
105
+ * ? ValidationResult.success({ value: port, defaulted: [] })
106
+ * : ValidationResult.fail({
107
+ * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],
108
+ * }),
109
+ * ),
110
+ * );
111
+ * ```
112
+ */
26
113
  transform(transform) {
27
- return new ScalarEnvNode(this.key, (value) => this.validateValue(value).flatMap(transform));
114
+ return new ScalarEnvNode(this.key, (value, path) => recoverScalarValidationResultOrEither(this.validateValue(value, path), path, this.key, value).flatMap((val) => recoverScalarValidationResultOrEither(transform(val, path), path, this.key, value)));
28
115
  }
29
116
  };
117
+ function recoverValidationResultOrEither(result) {
118
+ if (result instanceof EitherBase) return result.fold((error) => ValidationResult.fail(error), (success) => ValidationResult.success({
119
+ value: success,
120
+ defaulted: []
121
+ }));
122
+ return result;
123
+ }
124
+ function recoverScalarValidationResultOrEither(result, path, key, value) {
125
+ if (result instanceof EitherBase) return result.fold((error) => ValidationResult.fail(typeof error === "object" ? error : { errors: [{
126
+ path,
127
+ key,
128
+ message: error,
129
+ value
130
+ }] }), (success) => ValidationResult.success({
131
+ value: success,
132
+ defaulted: []
133
+ }));
134
+ return result;
135
+ }
30
136
 
31
137
  //#endregion
32
138
  export { EnvNode, ScalarEnvNode };
package/dist/ast.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ast.mjs","names":[],"sources":["../src/ast.ts"],"sourcesContent":["import type { ValidationResult } from './validation.js';\nimport { Either } from '@jeengbe/prelude';\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) => this.validate(path, loadValue).flatMap(transform));\n }\n}\n\nexport class ScalarEnvNode<T> extends EnvNode<T> {\n constructor(\n readonly key: string,\n private readonly validateValue: (value: string | undefined) => 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 override transform<U>(transform: (value: T) => ValidationResult<U>): ScalarEnvNode<U> {\n return new ScalarEnvNode(this.key, (value) => this.validateValue(value).flatMap(transform));\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 ? { [K in keyof U]: Pretty<U[K]> } : T;\n"],"mappings":";;;AAKA,IAAa,UAAb,MAAa,QAAW;CAIX;CADX,YACE,AAAS,UAIT;EAJS;CAIR;CAEH,UAAa,WAA0D;EACrE,OAAO,IAAI,SAAS,MAAM,cAAc,KAAK,SAAS,MAAM,SAAS,CAAC,CAAC,QAAQ,SAAS,CAAC;CAC3F;AACF;AAEA,IAAa,gBAAb,MAAa,sBAAyB,QAAW;CAEpC;CACQ;CAFnB,YACE,AAAS,KACT,AAAiB,eACjB;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;EAPQ;EACQ;CAOnB;CAEA,WAAyC;EACvC,OAAO,IAAI,cAA6B,KAAK,MAAM,UACjD,UAAU,SAAY,OAAO,MAAM,MAAS,IAAI,KAAK,cAAc,KAAK,CAC1E;CACF;CAEA,AAAS,UAAa,WAAgE;EACpF,OAAO,IAAI,cAAc,KAAK,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC,CAAC,QAAQ,SAAS,CAAC;CAC5F;AACF"}
1
+ {"version":3,"file":"ast.mjs","names":[],"sources":["../src/ast.ts"],"sourcesContent":["import { ValidationResult } from './validation.js';\nimport { Either, EitherBase } from '@jeengbe/prelude';\n\n// oxlint-disable-next-line no-unused-vars -- Imported for JSDoc\nimport type { Env } from './env.js';\n// oxlint-disable-next-line no-unused-vars -- Imported for JSDoc\nimport type { ValidationError, ValidationFailure } from './validation.js';\n\nconst nodeType = Symbol('type');\n\n/**\n * A node in a config schema that validates a value out of the loaded environment.\n *\n * Do not instantiate this class directly; use the {@link Env} interface instead.\n */\nexport class EnvNode<T> {\n declare private readonly [nodeType]: T;\n readonly validate: (\n loadValue: (key: string) => string | undefined,\n path: string,\n ) => ValidationResult<T>;\n\n constructor(\n validate: (\n loadValue: (key: string) => string | undefined,\n path: string,\n ) => ValidationResultOrEither<T>,\n ) {\n this.validate = (loadValue, path) => recoverValidationResultOrEither(validate(loadValue, path));\n }\n\n /**\n * Transforms the validated value of this node into a new value, or fails validation.\n *\n * @example\n *\n * ```ts\n * const res = env.load(\n * // ^? number\n * env.number('PORT').transform((port, path) =>\n * port > 0\n * ? ValidationResult.success({ value: port, defaulted: [] })\n * : ValidationResult.fail({\n * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],\n * }),\n * ),\n * );\n * ```\n */\n transform<U>(transform: (value: T, path: string) => ValidationResultOrEither<U>): EnvNode<U> {\n return new EnvNode((loadValue, path) =>\n this.validate(loadValue, path).flatMap((value) =>\n recoverValidationResultOrEither(transform(value, path)),\n ),\n );\n }\n}\n\n/**\n * An {@link EnvNode} backed by a single environment variable.\n *\n * Do not instantiate this class directly; use the {@link Env} interface instead.\n */\nexport class ScalarEnvNode<T> extends EnvNode<T> {\n constructor(\n readonly key: string,\n private readonly validateValue: (\n value: string | undefined,\n path: string,\n ) => ScalarValidationResultOrEither<T>,\n ) {\n super((loadValue, path) => {\n const value = loadValue(key);\n\n return recoverScalarValidationResultOrEither(validateValue(value, path), path, key, value);\n });\n }\n\n /**\n * Returns a new ScalarEnvNode that resolves to undefined instead of failing validation when the environment variable is not set.\n *\n * @example\n *\n * ```ts\n * const res = env.load(\n * // ^? number | undefined\n * env.number('PORT').optional(),\n * );\n *\n * // PORT=3000 -> 3000\n * // PORT= -> error\n * // (not set) -> undefined\n * ```\n */\n optional(): ScalarEnvNode<T | undefined> {\n return new ScalarEnvNode<T | undefined>(this.key, (value, path) =>\n value === undefined\n ? ValidationResult.success({\n value: undefined,\n defaulted: [\n {\n path,\n key: this.key,\n defaultValue: undefined,\n },\n ],\n })\n : this.validateValue(value, path),\n );\n }\n\n /**\n * Transforms the validated value of this node into a new value, or fails validation.\n *\n * As a shorthand for the common case of a single error message, `transform` may return a plain\n * `Either<string, U>` instead of a full `ValidationResult<U>`. The error string is automatically\n * wrapped into a {@link ValidationError} using this node's key, its path, and the value that failed to\n * transform.\n *\n * @example\n *\n * ```ts\n * const res = env.load(\n * // ^? number\n * env.number('PORT').transform((port) =>\n * port > 0 ? Either.right(port) : Either.left('must be positive'),\n * ),\n * );\n * ```\n *\n * @example\n *\n * ```ts\n * const res = env.load(\n * // ^? number\n * env.number('PORT').transform((port, path) =>\n * port > 0\n * ? ValidationResult.success({ value: port, defaulted: [] })\n * : ValidationResult.fail({\n * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],\n * }),\n * ),\n * );\n * ```\n */\n override transform<U>(\n transform: (value: T, path: string) => ScalarValidationResultOrEither<U>,\n ): ScalarEnvNode<U> {\n return new ScalarEnvNode(this.key, (value, path) =>\n recoverScalarValidationResultOrEither(\n this.validateValue(value, path),\n path,\n this.key,\n value,\n ).flatMap((val) =>\n recoverScalarValidationResultOrEither(transform(val, path), path, this.key, value),\n ),\n );\n }\n}\n\nexport type ValidationResultOrEither<T> = ValidationResult<T> | Either<ValidationFailure, T>;\n\nfunction recoverValidationResultOrEither<T>(\n result: ValidationResultOrEither<T>,\n): ValidationResult<T> {\n if (result instanceof EitherBase) {\n return result.fold(\n (error) => ValidationResult.fail(error),\n (success) =>\n ValidationResult.success({\n value: success,\n defaulted: [],\n }),\n );\n }\n\n return result;\n}\n\nexport type ScalarValidationResultOrEither<T> = ValidationResultOrEither<T> | Either<string, T>;\n\nfunction recoverScalarValidationResultOrEither<T>(\n result: ScalarValidationResultOrEither<T>,\n path: string,\n key: string,\n value: unknown,\n): ValidationResult<T> {\n if (result instanceof EitherBase) {\n return result.fold(\n (error) =>\n ValidationResult.fail(\n typeof error === 'object'\n ? error\n : {\n errors: [\n {\n path,\n key,\n message: error,\n value,\n },\n ],\n },\n ),\n (success) =>\n ValidationResult.success({\n value: success,\n defaulted: [],\n }),\n );\n }\n\n return result;\n}\n\n/**\n * Describes the shape of a config schema: either a single EnvNode, or a nested object of them.\n */\nexport type EnvSpec = EnvNode<unknown> | { [key: string]: EnvSpec };\n\n/**\n * Infers the resulting value type produced by loading an EnvSpec.\n *\n * @example\n * ```ts\n * export function createModuleConfig() {\n * return {\n * url: env.string('URL'),\n * port: env.number('PORT').optional(),\n * };\n * }\n *\n * export type ModuleConfig = ParseEnv<ReturnType<typeof createModuleConfig>>;\n * // ^? { url: string; port?: number | undefined; }\n * ```\n */\nexport type ParseEnv<T extends EnvSpec> =\n T extends EnvNode<infer U>\n ? U\n : T extends Record<string, EnvSpec>\n ? { [K in keyof T]: ParseEnv<T[K]> }\n : never;\n\nexport type Pretty<T> = T extends infer U extends object ? { [K in keyof U]: Pretty<U[K]> } : T;\n"],"mappings":";;;;;;;;;AAeA,IAAa,UAAb,MAAa,QAAW;CAEtB,AAAS;CAKT,YACE,UAIA;EACA,KAAK,YAAY,WAAW,SAAS,gCAAgC,SAAS,WAAW,IAAI,CAAC;CAChG;;;;;;;;;;;;;;;;;;;CAoBA,UAAa,WAAgF;EAC3F,OAAO,IAAI,SAAS,WAAW,SAC7B,KAAK,SAAS,WAAW,IAAI,CAAC,CAAC,SAAS,UACtC,gCAAgC,UAAU,OAAO,IAAI,CAAC,CACxD,CACF;CACF;AACF;;;;;;AAOA,IAAa,gBAAb,MAAa,sBAAyB,QAAW;CAEpC;CACQ;CAFnB,YACE,AAAS,KACT,AAAiB,eAIjB;EACA,OAAO,WAAW,SAAS;GACzB,MAAM,QAAQ,UAAU,GAAG;GAE3B,OAAO,sCAAsC,cAAc,OAAO,IAAI,GAAG,MAAM,KAAK,KAAK;EAC3F,CAAC;EAVQ;EACQ;CAUnB;;;;;;;;;;;;;;;;;CAkBA,WAAyC;EACvC,OAAO,IAAI,cAA6B,KAAK,MAAM,OAAO,SACxD,UAAU,SACN,iBAAiB,QAAQ;GACvB,OAAO;GACP,WAAW,CACT;IACE;IACA,KAAK,KAAK;IACV,cAAc;GAChB,CACF;EACF,CAAC,IACD,KAAK,cAAc,OAAO,IAAI,CACpC;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCA,AAAS,UACP,WACkB;EAClB,OAAO,IAAI,cAAc,KAAK,MAAM,OAAO,SACzC,sCACE,KAAK,cAAc,OAAO,IAAI,GAC9B,MACA,KAAK,KACL,KACF,CAAC,CAAC,SAAS,QACT,sCAAsC,UAAU,KAAK,IAAI,GAAG,MAAM,KAAK,KAAK,KAAK,CACnF,CACF;CACF;AACF;AAIA,SAAS,gCACP,QACqB;CACrB,IAAI,kBAAkB,YACpB,OAAO,OAAO,MACX,UAAU,iBAAiB,KAAK,KAAK,IACrC,YACC,iBAAiB,QAAQ;EACvB,OAAO;EACP,WAAW,CAAC;CACd,CAAC,CACL;CAGF,OAAO;AACT;AAIA,SAAS,sCACP,QACA,MACA,KACA,OACqB;CACrB,IAAI,kBAAkB,YACpB,OAAO,OAAO,MACX,UACC,iBAAiB,KACf,OAAO,UAAU,WACb,QACA,EACE,QAAQ,CACN;EACE;EACA;EACA,SAAS;EACT;CACF,CACF,EACF,CACN,IACD,YACC,iBAAiB,QAAQ;EACvB,OAAO;EACP,WAAW,CAAC;CACd,CAAC,CACL;CAGF,OAAO;AACT"}
package/dist/env.d.mts CHANGED
@@ -1,18 +1,216 @@
1
1
  import { ValidationResult } from "./validation.mjs";
2
- import { EnvNode, EnvSpec, InferEnvSpec, Pretty, ScalarEnvNode } from "./ast.mjs";
2
+ import { EnvNode, EnvSpec, ParseEnv, Pretty, ScalarEnvNode, ScalarValidationResultOrEither } from "./ast.mjs";
3
3
  //#region src/env.d.ts
4
+ /**
5
+ * Type-safe configuration loader.
6
+ *
7
+ * @example
8
+ *
9
+ * ```ts
10
+ * import { env } from '@jeengbe/config';
11
+ *
12
+ * const config = env.load({
13
+ * port: env.number('PORT', 3000),
14
+ * debug: env.boolean('DEBUG', false),
15
+ * driver: env.discriminate('driver', env.enum('DRIVER', ['memory', 'redis']), {
16
+ * memory: {},
17
+ * redis: { url: env.string('REDIS_URL') },
18
+ * }),
19
+ * });
20
+ *
21
+ * // -> {
22
+ * // port: number;
23
+ * // debug: boolean;
24
+ * // driver: { driver: 'memory' } | { driver: 'redis'; url: string };
25
+ * // }
26
+ * ```
27
+ *
28
+ * Variables are read from `process.env` and trimmed before validation. Only a fully unset variable
29
+ * falls back to a default value or fails as required - an explicitly empty value is passed through to
30
+ * validation like any other input.
31
+ */
4
32
  interface Env {
33
+ /**
34
+ * Reads a string environment variable.
35
+ *
36
+ * @example
37
+ *
38
+ * ```ts
39
+ * const res = env.load(env.string('API_KEY'));
40
+ * // ^? string
41
+ *
42
+ * // API_KEY=abc-test -> 'abc-test'
43
+ * // API_KEY=123 -> '123'
44
+ * // API_KEY= -> ''
45
+ * ```
46
+ */
5
47
  string(key: string, defaultValue?: string): ScalarEnvNode<string>;
48
+ /**
49
+ * Reads a numeric environment variable. Must match `/^-?\d+(?:\.\d+)?$/`, i.e. `[-]digits[.digits]`.
50
+ *
51
+ * @example
52
+ *
53
+ * ```ts
54
+ * const res = env.load(env.number('PORT'));
55
+ * // ^? number
56
+ *
57
+ * // PORT=3000 -> 3000
58
+ * // PORT=-1 -> -1
59
+ * // PORT=abc -> error
60
+ * ```
61
+ */
6
62
  number(key: string, defaultValue?: number): ScalarEnvNode<number>;
63
+ /**
64
+ * Reads a boolean environment variable (must be 'true' or 'false').
65
+ *
66
+ * @example
67
+ *
68
+ * ```ts
69
+ * const res = env.load(env.boolean('DEBUG'));
70
+ * // ^? boolean
71
+ *
72
+ * // DEBUG=true -> true
73
+ * // DEBUG=false -> false
74
+ * // DEBUG=abc -> error
75
+ * ```
76
+ */
7
77
  boolean(key: string, defaultValue?: boolean): ScalarEnvNode<boolean>;
78
+ /**
79
+ * Reads an environment variable constrained to one of the provided values.
80
+ *
81
+ * @example
82
+ *
83
+ * ```ts
84
+ * const res = env.load(env.enum('DRIVER', ['memory', 'redis']));
85
+ * // ^? 'memory' | 'redis'
86
+ *
87
+ * // DRIVER=memory -> 'memory'
88
+ * // DRIVER=abc -> error
89
+ * ```
90
+ */
8
91
  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[]>;
92
+ /**
93
+ * Reads a comma-separated list environment variable, validating each item against the provided item type.
94
+ *
95
+ * @example
96
+ *
97
+ * ```ts
98
+ * const res = env.load(env.array(env.string('API_KEYS')));
99
+ * // ^? readonly string[]
100
+ *
101
+ * // API_KEYS=abc,def,ghi -> ['abc', 'def', 'ghi']
102
+ * // API_KEYS= -> []
103
+ * // API_KEYS=abc,,ghi -> error
104
+ * ```
105
+ *
106
+ * @example
107
+ *
108
+ * ```ts
109
+ * const res = env.load(env.array(env.string('API_KEYS'), ['default1', 'default2']));
110
+ * // ^? readonly string[]
111
+ *
112
+ * // API_KEYS=abc,def,ghi -> ['abc', 'def', 'ghi']
113
+ * // API_KEYS= -> []
114
+ * // (not set) -> ['default1', 'default2']
115
+ * // API_KEYS=abc,,ghi -> error
116
+ * ```
117
+ *
118
+ * @example
119
+ *
120
+ * ```ts
121
+ * const res = env.load(env.array(env.string('API_KEYS').optional()));
122
+ * // ^? readonly (string | undefined)[]
123
+ *
124
+ * // API_KEYS=abc,def,ghi -> ['abc', 'def', 'ghi']
125
+ * // API_KEYS= -> []
126
+ * // API_KEYS=abc,,ghi -> ['abc', undefined, 'ghi']
127
+ * ```
128
+ */
129
+ array<const T>(itemType: ScalarEnvNode<T>, defaultValue?: readonly T[]): ScalarEnvNode<readonly T[]>;
130
+ /**
131
+ * Reads an environment variable, validating and transforming it with the provided function.
132
+ *
133
+ * @example
134
+ *
135
+ * ```ts
136
+ * const res = env.load(
137
+ * // ^? string
138
+ * env.scalar('TAG', (value, path) =>
139
+ * value.length === 3
140
+ * ? ValidationResult.success({ value, defaulted: [] })
141
+ * : ValidationResult.fail({
142
+ * errors: [{ path, key: 'TAG', message: 'must be 3 characters long', value }],
143
+ * }),
144
+ * ),
145
+ * );
146
+ *
147
+ * // TAG=abc -> 'abc'
148
+ * // TAG=abcd -> error
149
+ * ```
150
+ */
151
+ scalar<const T>(key: string, transform: (value: string, path: string) => ScalarValidationResultOrEither<T>, defaultValue?: T): ScalarEnvNode<T>;
152
+ /**
153
+ * Reads a discriminator environment variable and resolves the nested schema mapped to its value.
154
+ *
155
+ * @example
156
+ *
157
+ * ```ts
158
+ * const res = env.load(
159
+ * // ^? { driver: 'memory'; } | { driver: 'redis'; url: string; }
160
+ * env.discriminate('driver', env.enum('DRIVER', ['memory', 'redis']), {
161
+ * memory: {},
162
+ * redis: { url: env.string('REDIS_URL') },
163
+ * }),
164
+ * );
165
+ *
166
+ * // DRIVER=memory -> { driver: 'memory' }
167
+ * // DRIVER=redis, REDIS_URL=redis://localhost -> { driver: 'redis', url: 'redis://localhost' }
168
+ * // DRIVER=redis, REDIS_URL= -> { driver: 'redis', url: '' }
169
+ * // DRIVER=abc -> error
170
+ * ```
171
+ */
11
172
  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>>;
173
+ /**
174
+ * Validates and loads the provided schema from `process.env`. Returns a `ValidationResult` that contains either the parsed values or a list of validation errors.
175
+ *
176
+ * @example
177
+ *
178
+ * ```ts
179
+ * const result = env.parse({
180
+ * port: env.number('PORT'),
181
+ * debug: env.boolean('DEBUG'),
182
+ * });
183
+ *
184
+ * if (result.getLeft()) {
185
+ * console.error('Validation failed:', result.getLeft());
186
+ * } else {
187
+ * const config = result.get()!;
188
+ * // -> { port: number; debug: boolean; }
189
+ * }
190
+ * ```
191
+ */
192
+ parse<S extends EnvSpec>(spec: S, env?: NodeJS.ProcessEnv): ValidationResult<Pretty<ParseEnv<S>>>;
193
+ /**
194
+ * Validates and loads the provided schema from `process.env`. Throws if any required variables are missing or invalid.
195
+ *
196
+ * @example
197
+ *
198
+ * ```ts
199
+ * const config = env.load({
200
+ * port: env.number('PORT', 3000),
201
+ * debug: env.boolean('DEBUG', false),
202
+ * }, console.log);
203
+ *
204
+ * // -> {
205
+ * // port: number;
206
+ * // debug: boolean;
207
+ * // }
208
+ * ```
209
+ */
210
+ load<S extends EnvSpec>(spec: S, env?: NodeJS.ProcessEnv): Pretty<ParseEnv<S>>;
13
211
  }
14
212
  declare const env: Env;
15
- type DiscriminatorResult<K extends string, V extends string, M extends Partial<Record<V, Record<string, EnvSpec>>>> = Pretty<V extends unknown ? Record<K, V> & InferEnvSpec<M[V] extends EnvSpec ? M[V] : {}> : never>;
213
+ type DiscriminatorResult<K extends string, V extends string, M extends Partial<Record<V, Record<string, EnvSpec>>>> = Pretty<V extends unknown ? Record<K, V> & ParseEnv<M[V] extends EnvSpec ? M[V] : {}> : never>;
16
214
  //#endregion
17
215
  export { Env, env };
18
216
  //# sourceMappingURL=env.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.mts","names":[],"sources":["../src/env.ts"],"mappings":";;;UAMiB;EACf,OAAO,aAAa,wBAAwB;EAC5C,OAAO,aAAa,wBAAwB;EAC5C,QAAQ,aAAa,yBAAyB;EAC9C,WAAW,kBACT,aACA,iBAAiB,KACjB,eAAe,IACd,cAAc;EACjB,OAAO,GACL,aACA,YAAY,kBAAkB,iBAAiB,IAC/C,eAAe,IACd,cAAc;EACjB,MAAM,GAAG,UAAU,cAAc,IAAI,wBAAwB,MAAM,iBAAiB;EACpF,aACE,kBACA,kBACA,UAAU,QAAQ,OAAO,GAAG,eAAe,YAE3C,kBAAkB,GAClB,wBAAwB,cAAc,IACtC,SAAS,IACR,QAAQ,oBAAoB,GAAG,GAAG;EACrC,KAAK,UAAU,SAAS,MAAM,IAAI,OAAO,aAAa;;cAG3C,KAAK;KAgHb,oBACH,kBACA,kBACA,UAAU,QAAQ,OAAO,GAAG,eAAe,cACzC,OAGF,oBAAoB,OAAO,GAAG,KAAK,aAAa,EAAE,WAAW,UAAU,EAAE"}
1
+ {"version":3,"file":"env.d.mts","names":[],"sources":["../src/env.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCiB;;;;;;;;;;;;;;;EAef,OAAO,aAAa,wBAAwB;;;;;;;;;;;;;;;EAgB5C,OAAO,aAAa,wBAAwB;;;;;;;;;;;;;;;EAgB5C,QAAQ,aAAa,yBAAyB;;;;;;;;;;;;;;EAe9C,WAAW,kBACT,aACA,iBAAiB,KACjB,eAAe,IACd,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuCjB,YAAY,GACV,UAAU,cAAc,IACxB,wBAAwB,MACvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;EAuB1B,aAAa,GACX,aACA,YAAY,eAAe,iBAAiB,+BAA+B,IAC3E,eAAe,IACd,cAAc;;;;;;;;;;;;;;;;;;;;;EAsBjB,aACE,kBACA,kBACA,UAAU,QAAQ,OAAO,GAAG,eAAe,YAE3C,kBAAkB,GAClB,wBAAwB,cAAc,IACtC,SAAS,IACR,QAAQ,oBAAoB,GAAG,GAAG;;;;;;;;;;;;;;;;;;;;EAqBrC,MAAM,UAAU,SAAS,MAAM,GAAG,MAAM,OAAO,aAAa,iBAAiB,OAAO,SAAS;;;;;;;;;;;;;;;;;;EAmB7F,KAAK,UAAU,SAAS,MAAM,GAAG,MAAM,OAAO,aAAa,OAAO,SAAS;;cAGhE,KAAK;KA+Lb,oBACH,kBACA,kBACA,UAAU,QAAQ,OAAO,GAAG,eAAe,cACzC,OAGF,oBAAoB,OAAO,GAAG,KAAK,SAAS,EAAE,WAAW,UAAU,EAAE"}