@jeengbe/config 0.0.8 → 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/src/ast.ts CHANGED
@@ -1,53 +1,245 @@
1
- import type { ValidationResult } from './validation.js';
2
- import { Either } from '@jeengbe/prelude';
1
+ import { ValidationResult } from './validation.js';
2
+ import { Either, EitherBase } from '@jeengbe/prelude';
3
+
4
+ // oxlint-disable-next-line no-unused-vars -- Imported for JSDoc
5
+ import type { Env } from './env.js';
6
+ // oxlint-disable-next-line no-unused-vars -- Imported for JSDoc
7
+ import type { ValidationError, ValidationFailure } from './validation.js';
3
8
 
4
9
  const nodeType = Symbol('type');
5
10
 
11
+ /**
12
+ * A node in a config schema that validates a value out of the loaded environment.
13
+ *
14
+ * Do not instantiate this class directly; use the {@link Env} interface instead.
15
+ */
6
16
  export class EnvNode<T> {
7
17
  declare private readonly [nodeType]: T;
18
+ readonly validate: (
19
+ loadValue: (key: string) => string | undefined,
20
+ path: string,
21
+ ) => ValidationResult<T>;
8
22
 
9
23
  constructor(
10
- readonly validate: (
11
- path: string,
24
+ validate: (
12
25
  loadValue: (key: string) => string | undefined,
13
- ) => ValidationResult<T>,
14
- ) {}
26
+ path: string,
27
+ ) => ValidationResultOrEither<T>,
28
+ ) {
29
+ this.validate = (loadValue, path) => recoverValidationResultOrEither(validate(loadValue, path));
30
+ }
15
31
 
16
- transform<U>(transform: (value: T) => ValidationResult<U>): EnvNode<U> {
17
- return new EnvNode((path, loadValue) => this.validate(path, loadValue).flatMap(transform));
32
+ /**
33
+ * Transforms the validated value of this node into a new value, or fails validation.
34
+ *
35
+ * @example
36
+ *
37
+ * ```ts
38
+ * const res = env.load(
39
+ * // ^? number
40
+ * env.number('PORT').transform((port, path) =>
41
+ * port > 0
42
+ * ? ValidationResult.success({ value: port, defaulted: [] })
43
+ * : ValidationResult.fail({
44
+ * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],
45
+ * }),
46
+ * ),
47
+ * );
48
+ * ```
49
+ */
50
+ transform<U>(transform: (value: T, path: string) => ValidationResultOrEither<U>): EnvNode<U> {
51
+ return new EnvNode((loadValue, path) =>
52
+ this.validate(loadValue, path).flatMap((value) =>
53
+ recoverValidationResultOrEither(transform(value, path)),
54
+ ),
55
+ );
18
56
  }
19
57
  }
20
58
 
59
+ /**
60
+ * An {@link EnvNode} backed by a single environment variable.
61
+ *
62
+ * Do not instantiate this class directly; use the {@link Env} interface instead.
63
+ */
21
64
  export class ScalarEnvNode<T> extends EnvNode<T> {
22
65
  constructor(
23
66
  readonly key: string,
24
- private readonly validateValue: (value: string | undefined) => ValidationResult<T>,
67
+ private readonly validateValue: (
68
+ value: string | undefined,
69
+ path: string,
70
+ ) => ScalarValidationResultOrEither<T>,
25
71
  ) {
26
- super((path, loadValue) => {
27
- return validateValue(loadValue(key)).leftMap((errors) =>
28
- errors.map((error) => `${key} (${path}): ${error}`),
29
- );
72
+ super((loadValue, path) => {
73
+ const value = loadValue(key);
74
+
75
+ return recoverScalarValidationResultOrEither(validateValue(value, path), path, key, value);
30
76
  });
31
77
  }
32
78
 
79
+ /**
80
+ * Returns a new ScalarEnvNode that resolves to undefined instead of failing validation when the environment variable is not set.
81
+ *
82
+ * @example
83
+ *
84
+ * ```ts
85
+ * const res = env.load(
86
+ * // ^? number | undefined
87
+ * env.number('PORT').optional(),
88
+ * );
89
+ *
90
+ * // PORT=3000 -> 3000
91
+ * // PORT= -> error
92
+ * // (not set) -> undefined
93
+ * ```
94
+ */
33
95
  optional(): ScalarEnvNode<T | undefined> {
34
- return new ScalarEnvNode<T | undefined>(this.key, (value) =>
35
- value === undefined ? Either.right(undefined) : this.validateValue(value),
96
+ return new ScalarEnvNode<T | undefined>(this.key, (value, path) =>
97
+ value === undefined
98
+ ? ValidationResult.success({
99
+ value: undefined,
100
+ defaulted: [
101
+ {
102
+ path,
103
+ key: this.key,
104
+ defaultValue: undefined,
105
+ },
106
+ ],
107
+ })
108
+ : this.validateValue(value, path),
36
109
  );
37
110
  }
38
111
 
39
- override transform<U>(transform: (value: T) => ValidationResult<U>): ScalarEnvNode<U> {
40
- return new ScalarEnvNode(this.key, (value) => this.validateValue(value).flatMap(transform));
112
+ /**
113
+ * Transforms the validated value of this node into a new value, or fails validation.
114
+ *
115
+ * As a shorthand for the common case of a single error message, `transform` may return a plain
116
+ * `Either<string, U>` instead of a full `ValidationResult<U>`. The error string is automatically
117
+ * wrapped into a {@link ValidationError} using this node's key, its path, and the value that failed to
118
+ * transform.
119
+ *
120
+ * @example
121
+ *
122
+ * ```ts
123
+ * const res = env.load(
124
+ * // ^? number
125
+ * env.number('PORT').transform((port) =>
126
+ * port > 0 ? Either.right(port) : Either.left('must be positive'),
127
+ * ),
128
+ * );
129
+ * ```
130
+ *
131
+ * @example
132
+ *
133
+ * ```ts
134
+ * const res = env.load(
135
+ * // ^? number
136
+ * env.number('PORT').transform((port, path) =>
137
+ * port > 0
138
+ * ? ValidationResult.success({ value: port, defaulted: [] })
139
+ * : ValidationResult.fail({
140
+ * errors: [{ path, key: 'PORT', message: 'must be positive', value: port }],
141
+ * }),
142
+ * ),
143
+ * );
144
+ * ```
145
+ */
146
+ override transform<U>(
147
+ transform: (value: T, path: string) => ScalarValidationResultOrEither<U>,
148
+ ): ScalarEnvNode<U> {
149
+ return new ScalarEnvNode(this.key, (value, path) =>
150
+ recoverScalarValidationResultOrEither(
151
+ this.validateValue(value, path),
152
+ path,
153
+ this.key,
154
+ value,
155
+ ).flatMap((val) =>
156
+ recoverScalarValidationResultOrEither(transform(val, path), path, this.key, value),
157
+ ),
158
+ );
41
159
  }
42
160
  }
43
161
 
162
+ export type ValidationResultOrEither<T> = ValidationResult<T> | Either<ValidationFailure, T>;
163
+
164
+ function recoverValidationResultOrEither<T>(
165
+ result: ValidationResultOrEither<T>,
166
+ ): ValidationResult<T> {
167
+ if (result instanceof EitherBase) {
168
+ return result.fold(
169
+ (error) => ValidationResult.fail(error),
170
+ (success) =>
171
+ ValidationResult.success({
172
+ value: success,
173
+ defaulted: [],
174
+ }),
175
+ );
176
+ }
177
+
178
+ return result;
179
+ }
180
+
181
+ export type ScalarValidationResultOrEither<T> = ValidationResultOrEither<T> | Either<string, T>;
182
+
183
+ function recoverScalarValidationResultOrEither<T>(
184
+ result: ScalarValidationResultOrEither<T>,
185
+ path: string,
186
+ key: string,
187
+ value: unknown,
188
+ ): ValidationResult<T> {
189
+ if (result instanceof EitherBase) {
190
+ return result.fold(
191
+ (error) =>
192
+ ValidationResult.fail(
193
+ typeof error === 'object'
194
+ ? error
195
+ : {
196
+ errors: [
197
+ {
198
+ path,
199
+ key,
200
+ message: error,
201
+ value,
202
+ },
203
+ ],
204
+ },
205
+ ),
206
+ (success) =>
207
+ ValidationResult.success({
208
+ value: success,
209
+ defaulted: [],
210
+ }),
211
+ );
212
+ }
213
+
214
+ return result;
215
+ }
216
+
217
+ /**
218
+ * Describes the shape of a config schema: either a single EnvNode, or a nested object of them.
219
+ */
44
220
  export type EnvSpec = EnvNode<unknown> | { [key: string]: EnvSpec };
45
221
 
46
- export type InferEnvSpec<T extends EnvSpec> =
222
+ /**
223
+ * Infers the resulting value type produced by loading an EnvSpec.
224
+ *
225
+ * @example
226
+ * ```ts
227
+ * export function createModuleConfig() {
228
+ * return {
229
+ * url: env.string('URL'),
230
+ * port: env.number('PORT').optional(),
231
+ * };
232
+ * }
233
+ *
234
+ * export type ModuleConfig = ParseEnv<ReturnType<typeof createModuleConfig>>;
235
+ * // ^? { url: string; port?: number | undefined; }
236
+ * ```
237
+ */
238
+ export type ParseEnv<T extends EnvSpec> =
47
239
  T extends EnvNode<infer U>
48
240
  ? U
49
241
  : T extends Record<string, EnvSpec>
50
- ? { [K in keyof T]: InferEnvSpec<T[K]> }
242
+ ? { [K in keyof T]: ParseEnv<T[K]> }
51
243
  : never;
52
244
 
53
245
  export type Pretty<T> = T extends infer U extends object ? { [K in keyof U]: Pretty<U[K]> } : T;