@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/README.md +127 -0
- package/dist/ast.d.mts +108 -8
- package/dist/ast.d.mts.map +1 -1
- package/dist/ast.mjs +113 -7
- package/dist/ast.mjs.map +1 -1
- package/dist/env.d.mts +203 -5
- package/dist/env.d.mts.map +1 -1
- package/dist/env.mjs +75 -35
- package/dist/env.mjs.map +1 -1
- package/dist/if-enabled.d.mts +13 -2
- package/dist/if-enabled.d.mts.map +1 -1
- package/dist/if-enabled.mjs +11 -0
- package/dist/if-enabled.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +2 -1
- package/dist/validation.d.mts +103 -6
- package/dist/validation.d.mts.map +1 -1
- package/dist/validation.mjs +109 -5
- package/dist/validation.mjs.map +1 -1
- package/package.json +6 -2
- package/src/ast.ts +211 -19
- package/src/env.ts +361 -73
- package/src/if-enabled.ts +15 -7
- package/src/index.ts +8 -1
- package/src/validation.ts +162 -12
package/src/ast.ts
CHANGED
|
@@ -1,53 +1,245 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
11
|
-
path: string,
|
|
24
|
+
validate: (
|
|
12
25
|
loadValue: (key: string) => string | undefined,
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
path: string,
|
|
27
|
+
) => ValidationResultOrEither<T>,
|
|
28
|
+
) {
|
|
29
|
+
this.validate = (loadValue, path) => recoverValidationResultOrEither(validate(loadValue, path));
|
|
30
|
+
}
|
|
15
31
|
|
|
16
|
-
|
|
17
|
-
|
|
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: (
|
|
67
|
+
private readonly validateValue: (
|
|
68
|
+
value: string | undefined,
|
|
69
|
+
path: string,
|
|
70
|
+
) => ScalarValidationResultOrEither<T>,
|
|
25
71
|
) {
|
|
26
|
-
super((
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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]:
|
|
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;
|