gruber 0.4.0 → 0.4.1
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/CHANGELOG.md +10 -0
- package/core/configuration.d.ts +4 -4
- package/core/configuration.js +17 -6
- package/core/configuration.test.js +3 -3
- package/core/structures.d.ts +14 -17
- package/core/structures.js +40 -1
- package/core/structures.test.js +88 -0
- package/package.json +1 -1
- package/source/configuration.d.ts +2 -2
- package/source/configuration.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
This file documents notable changes to the project
|
|
4
4
|
|
|
5
|
+
## 0.4.1
|
|
6
|
+
|
|
7
|
+
**fixes**
|
|
8
|
+
|
|
9
|
+
- `getDenoConfigOptions`, `getDenoConfiguration`, `getNodeConfigOptions` and `getNodeConfiguration` all have a default options of `{}`
|
|
10
|
+
- The Configuration markdown tables calculates the width properly when there are non-strings (URLs) in there
|
|
11
|
+
- The `Structure.boolean` method correctly types the optional fallback argument.
|
|
12
|
+
- Add experimental `Structure.literal` construct
|
|
13
|
+
- `Structure.object` fails if there are additional fields or the value is an instance of a class
|
|
14
|
+
|
|
5
15
|
## 0.4.0
|
|
6
16
|
|
|
7
17
|
**new**
|
package/core/configuration.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class _ObjectSpec {
|
|
|
38
38
|
fields: Record<string, string>[];
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
export class
|
|
41
|
+
export class _PrimativeSpec {
|
|
42
42
|
/**
|
|
43
43
|
* @param {string} type
|
|
44
44
|
* @param {SpecOptions<any>} options
|
|
@@ -49,11 +49,11 @@ export class _LiteralSpec {
|
|
|
49
49
|
describe(name: any): {
|
|
50
50
|
fallback: any;
|
|
51
51
|
fields: {
|
|
52
|
-
variable?: string;
|
|
53
|
-
flag?: string;
|
|
54
|
-
fallback: any;
|
|
55
52
|
name: any;
|
|
56
53
|
type: string;
|
|
54
|
+
fallback: any;
|
|
55
|
+
variable?: string;
|
|
56
|
+
flag?: string;
|
|
57
57
|
}[];
|
|
58
58
|
};
|
|
59
59
|
}
|
package/core/configuration.js
CHANGED
|
@@ -70,7 +70,11 @@ export class _ObjectSpec {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
//
|
|
74
|
+
// NOTE: describe() calls should return the actual value in "fallback"
|
|
75
|
+
// and the string-value in fields
|
|
76
|
+
//
|
|
77
|
+
export class _PrimativeSpec {
|
|
74
78
|
/**
|
|
75
79
|
* @param {string} type
|
|
76
80
|
* @param {SpecOptions<any>} options
|
|
@@ -82,7 +86,14 @@ export class _LiteralSpec {
|
|
|
82
86
|
describe(name) {
|
|
83
87
|
return {
|
|
84
88
|
fallback: this.options.fallback,
|
|
85
|
-
fields: [
|
|
89
|
+
fields: [
|
|
90
|
+
{
|
|
91
|
+
...this.options,
|
|
92
|
+
name,
|
|
93
|
+
type: this.type,
|
|
94
|
+
fallback: this.options.fallback?.toString(),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
86
97
|
};
|
|
87
98
|
}
|
|
88
99
|
}
|
|
@@ -150,7 +161,7 @@ export class Configuration {
|
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
const struct = Structure.string(this._getValue(options).value);
|
|
153
|
-
struct[Configuration.spec] = new
|
|
164
|
+
struct[Configuration.spec] = new _PrimativeSpec("string", options);
|
|
154
165
|
return struct;
|
|
155
166
|
}
|
|
156
167
|
|
|
@@ -165,7 +176,7 @@ export class Configuration {
|
|
|
165
176
|
|
|
166
177
|
const fallback = this._parseFloat(this._getValue(options));
|
|
167
178
|
const struct = Structure.number(fallback);
|
|
168
|
-
struct[Configuration.spec] = new
|
|
179
|
+
struct[Configuration.spec] = new _PrimativeSpec("number", options);
|
|
169
180
|
return struct;
|
|
170
181
|
}
|
|
171
182
|
|
|
@@ -180,7 +191,7 @@ export class Configuration {
|
|
|
180
191
|
|
|
181
192
|
const fallback = this._parseBoolean(this._getValue(options));
|
|
182
193
|
const struct = Structure.boolean(fallback);
|
|
183
|
-
struct[Configuration.spec] = new
|
|
194
|
+
struct[Configuration.spec] = new _PrimativeSpec("boolean", options);
|
|
184
195
|
return struct;
|
|
185
196
|
}
|
|
186
197
|
|
|
@@ -196,7 +207,7 @@ export class Configuration {
|
|
|
196
207
|
throw new TypeError("options.fallback must be a string or URL");
|
|
197
208
|
}
|
|
198
209
|
const struct = Structure.url(this._getValue(options).value);
|
|
199
|
-
struct[Configuration.spec] = new
|
|
210
|
+
struct[Configuration.spec] = new _PrimativeSpec("url", {
|
|
200
211
|
...options,
|
|
201
212
|
fallback: new URL(options.fallback),
|
|
202
213
|
});
|
|
@@ -56,7 +56,7 @@ describe("Configuration", () => {
|
|
|
56
56
|
name: "person.age",
|
|
57
57
|
type: "number",
|
|
58
58
|
flag: "--age",
|
|
59
|
-
fallback: 42,
|
|
59
|
+
fallback: "42",
|
|
60
60
|
},
|
|
61
61
|
],
|
|
62
62
|
});
|
|
@@ -381,7 +381,7 @@ describe("Configuration", () => {
|
|
|
381
381
|
const config = new Configuration(bareOptions);
|
|
382
382
|
const result = config.describe(
|
|
383
383
|
config.url({
|
|
384
|
-
fallback: "https://example.com",
|
|
384
|
+
fallback: "https://example.com/",
|
|
385
385
|
variable: "SELF_URL",
|
|
386
386
|
flag: "--self-url",
|
|
387
387
|
}),
|
|
@@ -392,7 +392,7 @@ describe("Configuration", () => {
|
|
|
392
392
|
{
|
|
393
393
|
name: "selfUrl",
|
|
394
394
|
type: "url",
|
|
395
|
-
fallback:
|
|
395
|
+
fallback: "https://example.com/",
|
|
396
396
|
variable: "SELF_URL",
|
|
397
397
|
flag: "--self-url",
|
|
398
398
|
},
|
package/core/structures.d.ts
CHANGED
|
@@ -18,17 +18,6 @@ export class StructError extends Error {
|
|
|
18
18
|
/** @returns {Iterator<StructError>} */
|
|
19
19
|
[Symbol.iterator](): Iterator<StructError>;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {Record<string,unknown>} Schema
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* @template T
|
|
26
|
-
* @typedef {(input?: unknown, context?: StructContext) => T} StructExec
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* @template T
|
|
30
|
-
* @typedef {T extends Structure<infer U> ? U : never} Infer
|
|
31
|
-
*/
|
|
32
21
|
/**
|
|
33
22
|
* @template T
|
|
34
23
|
*/
|
|
@@ -44,10 +33,10 @@ export class Structure<T> {
|
|
|
44
33
|
*/
|
|
45
34
|
static number(fallback?: number): Structure<number>;
|
|
46
35
|
/**
|
|
47
|
-
* @param {boolean} fallback
|
|
36
|
+
* @param {boolean} [fallback]
|
|
48
37
|
* @returns {Structure<boolean>}
|
|
49
38
|
*/
|
|
50
|
-
static boolean(fallback
|
|
39
|
+
static boolean(fallback?: boolean): Structure<boolean>;
|
|
51
40
|
/**
|
|
52
41
|
* @param {string | URL} [fallback]
|
|
53
42
|
* @returns {Structure<URL>}
|
|
@@ -67,17 +56,28 @@ export class Structure<T> {
|
|
|
67
56
|
* @returns {Structure<Array<Infer<U>>>}
|
|
68
57
|
*/
|
|
69
58
|
static array<U_1 extends Structure<any>>(struct: U_1): Structure<Infer<U_1>[]>;
|
|
59
|
+
/**
|
|
60
|
+
* **UNSTABLE** use at your own risk
|
|
61
|
+
*
|
|
62
|
+
* @template {string|number|boolean} T
|
|
63
|
+
* @param {T} value
|
|
64
|
+
* @returns {Structure<T>}
|
|
65
|
+
*/
|
|
66
|
+
static literal<T_1 extends string | number | boolean>(value: T_1): Structure<T_1>;
|
|
70
67
|
/**
|
|
71
68
|
* @param {Schema} schema
|
|
72
69
|
* @param {StructExec<T>} process
|
|
73
70
|
*/
|
|
74
71
|
constructor(schema: Schema, process: StructExec<T>);
|
|
75
|
-
schema:
|
|
72
|
+
schema: Record<string, unknown>;
|
|
76
73
|
process: StructExec<T>;
|
|
77
74
|
getSchema(): {
|
|
78
75
|
$schema: string;
|
|
79
76
|
};
|
|
80
77
|
}
|
|
78
|
+
export type Schema = Record<string, unknown>;
|
|
79
|
+
export type StructExec<T> = (input?: unknown, context?: StructContext) => T;
|
|
80
|
+
export type Infer<T> = T extends Structure<infer U> ? U : never;
|
|
81
81
|
export type StructContext = {
|
|
82
82
|
path: string[];
|
|
83
83
|
};
|
|
@@ -86,6 +86,3 @@ export type StructOptions<Type, Schema> = {
|
|
|
86
86
|
schema: Schema;
|
|
87
87
|
fn: (value: unknown, ctx: StructContext) => Type;
|
|
88
88
|
};
|
|
89
|
-
export type Schema = Record<string, unknown>;
|
|
90
|
-
export type StructExec<T> = (input?: unknown, context?: StructContext) => T;
|
|
91
|
-
export type Infer<T> = T extends Structure<infer U> ? U : never;
|
package/core/structures.js
CHANGED
|
@@ -85,6 +85,11 @@ export class StructError extends Error {
|
|
|
85
85
|
* @typedef {T extends Structure<infer U> ? U : never} Infer
|
|
86
86
|
*/
|
|
87
87
|
|
|
88
|
+
function _additionalProperties(fields, input) {
|
|
89
|
+
const allowed = new Set(Object.keys(fields));
|
|
90
|
+
return Array.from(Object.keys(input)).filter((key) => !allowed.has(key));
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
/**
|
|
89
94
|
* @template T
|
|
90
95
|
*/
|
|
@@ -148,7 +153,7 @@ export class Structure {
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/**
|
|
151
|
-
* @param {boolean} fallback
|
|
156
|
+
* @param {boolean} [fallback]
|
|
152
157
|
* @returns {Structure<boolean>}
|
|
153
158
|
*/
|
|
154
159
|
static boolean(fallback) {
|
|
@@ -206,6 +211,9 @@ export class Structure {
|
|
|
206
211
|
if (input && typeof input !== "object") {
|
|
207
212
|
throw new StructError("Expected an object", path);
|
|
208
213
|
}
|
|
214
|
+
if (Object.getPrototypeOf(input) !== Object.getPrototypeOf({})) {
|
|
215
|
+
throw new StructError("Should not have a prototype", path);
|
|
216
|
+
}
|
|
209
217
|
const output = {};
|
|
210
218
|
const errors = [];
|
|
211
219
|
for (const key in fields) {
|
|
@@ -216,6 +224,13 @@ export class Structure {
|
|
|
216
224
|
errors.push(StructError.chain(error, ctx));
|
|
217
225
|
}
|
|
218
226
|
}
|
|
227
|
+
|
|
228
|
+
for (const key of _additionalProperties(fields, input)) {
|
|
229
|
+
errors.push(
|
|
230
|
+
new StructError("Additional field not allowed", [...path, key]),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
219
234
|
if (errors.length > 0) {
|
|
220
235
|
throw new StructError("Object does not match schema", path, errors);
|
|
221
236
|
}
|
|
@@ -257,4 +272,28 @@ export class Structure {
|
|
|
257
272
|
return output;
|
|
258
273
|
});
|
|
259
274
|
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* **UNSTABLE** use at your own risk
|
|
278
|
+
*
|
|
279
|
+
* @template {string|number|boolean} T
|
|
280
|
+
* @param {T} value
|
|
281
|
+
* @returns {Structure<T>}
|
|
282
|
+
*/
|
|
283
|
+
static literal(value) {
|
|
284
|
+
const schema = { type: typeof value, const: value };
|
|
285
|
+
|
|
286
|
+
return new Structure(schema, (input, context = undefined) => {
|
|
287
|
+
if (input === undefined) {
|
|
288
|
+
throw new StructError("Missing value", context?.path);
|
|
289
|
+
}
|
|
290
|
+
if (input !== value) {
|
|
291
|
+
throw new StructError(
|
|
292
|
+
`Expected ${schema.type} literal: ${value}`,
|
|
293
|
+
context?.path,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
260
299
|
}
|
package/core/structures.test.js
CHANGED
|
@@ -425,6 +425,44 @@ describe("Structure", () => {
|
|
|
425
425
|
assertEquals(error.children[0].message, "Expected a string");
|
|
426
426
|
assertEquals(error.children[0].path, ["key"], "should capture context");
|
|
427
427
|
});
|
|
428
|
+
it("throws for unknown fields", () => {
|
|
429
|
+
const struct = Structure.object({
|
|
430
|
+
key: Structure.string("fallback"),
|
|
431
|
+
});
|
|
432
|
+
const error = assertThrows(
|
|
433
|
+
() =>
|
|
434
|
+
struct.process(
|
|
435
|
+
{ key: "value", something: "else" },
|
|
436
|
+
{ path: ["some", "path"] },
|
|
437
|
+
),
|
|
438
|
+
StructError,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
assertEquals(error.message, "Object does not match schema");
|
|
442
|
+
assertEquals(error.path, ["some", "path"], "should capture the context");
|
|
443
|
+
|
|
444
|
+
assertEquals(error.children[0].message, "Additional field not allowed");
|
|
445
|
+
assertEquals(
|
|
446
|
+
error.children[0].path,
|
|
447
|
+
["some", "path", "something"],
|
|
448
|
+
"should capture the context",
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
it("throws for non-null prototypes", () => {
|
|
452
|
+
const struct = Structure.object({
|
|
453
|
+
key: Structure.string("fallback"),
|
|
454
|
+
});
|
|
455
|
+
class Injector {
|
|
456
|
+
key = "value";
|
|
457
|
+
}
|
|
458
|
+
const error = assertThrows(
|
|
459
|
+
() => struct.process(new Injector(), { path: ["some", "path"] }),
|
|
460
|
+
StructError,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
assertEquals(error.message, "Should not have a prototype");
|
|
464
|
+
assertEquals(error.path, ["some", "path"], "should capture the context");
|
|
465
|
+
});
|
|
428
466
|
});
|
|
429
467
|
|
|
430
468
|
describe("array", () => {
|
|
@@ -471,4 +509,54 @@ describe("Structure", () => {
|
|
|
471
509
|
assertEquals(error.children[0].path, ["1"], "should capture context");
|
|
472
510
|
});
|
|
473
511
|
});
|
|
512
|
+
|
|
513
|
+
describe("literal", () => {
|
|
514
|
+
it("creates a structure", () => {
|
|
515
|
+
const struct = Structure.literal(42);
|
|
516
|
+
assertInstanceOf(struct, Structure);
|
|
517
|
+
});
|
|
518
|
+
it("allows that value", () => {
|
|
519
|
+
const struct = Structure.literal(42);
|
|
520
|
+
assertEquals(struct.process(42), 42, "should pass the value through");
|
|
521
|
+
});
|
|
522
|
+
it("throws for different values", () => {
|
|
523
|
+
const struct = Structure.literal(42);
|
|
524
|
+
|
|
525
|
+
const error = assertThrows(
|
|
526
|
+
() => struct.process(69, { path: ["some", "path"] }),
|
|
527
|
+
StructError,
|
|
528
|
+
);
|
|
529
|
+
assertEquals(
|
|
530
|
+
error,
|
|
531
|
+
new StructError("Expected number literal: 42", ["some", "path"]),
|
|
532
|
+
"should throw a StructError and capture the context",
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
it("throws for different types", () => {
|
|
536
|
+
const struct = Structure.literal(42);
|
|
537
|
+
|
|
538
|
+
const error = assertThrows(
|
|
539
|
+
() => struct.process("nice", { path: ["some", "path"] }),
|
|
540
|
+
StructError,
|
|
541
|
+
);
|
|
542
|
+
assertEquals(
|
|
543
|
+
error,
|
|
544
|
+
new StructError("Expected number literal: 42", ["some", "path"]),
|
|
545
|
+
"should throw a StructError and capture the context",
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
it("throws for missing values", () => {
|
|
549
|
+
const struct = Structure.literal(42);
|
|
550
|
+
|
|
551
|
+
const error = assertThrows(
|
|
552
|
+
() => struct.process(undefined, { path: ["some", "path"] }),
|
|
553
|
+
StructError,
|
|
554
|
+
);
|
|
555
|
+
assertEquals(
|
|
556
|
+
error,
|
|
557
|
+
new StructError("Missing value", ["some", "path"]),
|
|
558
|
+
"should throw a StructError and capture the context",
|
|
559
|
+
);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
474
562
|
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
@typedef {object} NodeConfigurationOptions
|
|
3
3
|
*/
|
|
4
4
|
/** @param {NodeConfigurationOptions} options */
|
|
5
|
-
export function getNodeConfigOptions(options
|
|
5
|
+
export function getNodeConfigOptions(options?: NodeConfigurationOptions): {
|
|
6
6
|
readTextFile(url: any): Promise<Buffer>;
|
|
7
7
|
getEnvironmentVariable(key: any): string;
|
|
8
8
|
getCommandArgument(key: any): string | boolean;
|
|
@@ -13,7 +13,7 @@ export function getNodeConfigOptions(options: NodeConfigurationOptions): {
|
|
|
13
13
|
* This is a syntax sugar for `new Configuration(getNodeConfigOptions(options))`
|
|
14
14
|
* @param {NodeConfigurationOptions} options
|
|
15
15
|
*/
|
|
16
|
-
export function getNodeConfiguration(options
|
|
16
|
+
export function getNodeConfiguration(options?: NodeConfigurationOptions): Configuration;
|
|
17
17
|
export { Configuration };
|
|
18
18
|
export type NodeConfigurationOptions = object;
|
|
19
19
|
import { Configuration } from "../core/configuration.js";
|
package/source/configuration.js
CHANGED
|
@@ -11,7 +11,7 @@ export { Configuration };
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/** @param {NodeConfigurationOptions} options */
|
|
14
|
-
export function getNodeConfigOptions(options) {
|
|
14
|
+
export function getNodeConfigOptions(options = {}) {
|
|
15
15
|
const args = util.parseArgs({
|
|
16
16
|
args: process.args,
|
|
17
17
|
strict: false,
|
|
@@ -43,6 +43,6 @@ export function getNodeConfigOptions(options) {
|
|
|
43
43
|
* This is a syntax sugar for `new Configuration(getNodeConfigOptions(options))`
|
|
44
44
|
* @param {NodeConfigurationOptions} options
|
|
45
45
|
*/
|
|
46
|
-
export function getNodeConfiguration(options) {
|
|
46
|
+
export function getNodeConfiguration(options = {}) {
|
|
47
47
|
return new Configuration(getNodeConfigOptions(options));
|
|
48
48
|
}
|