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 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**
@@ -38,7 +38,7 @@ export class _ObjectSpec {
38
38
  fields: Record<string, string>[];
39
39
  };
40
40
  }
41
- export class _LiteralSpec {
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
  }
@@ -70,7 +70,11 @@ export class _ObjectSpec {
70
70
  }
71
71
  }
72
72
 
73
- export class _LiteralSpec {
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: [{ name, type: this.type, ...this.options }],
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 _LiteralSpec("string", options);
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 _LiteralSpec("number", options);
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 _LiteralSpec("boolean", options);
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 _LiteralSpec("url", {
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: new URL("https://example.com"),
395
+ fallback: "https://example.com/",
396
396
  variable: "SELF_URL",
397
397
  flag: "--self-url",
398
398
  },
@@ -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: boolean): Structure<boolean>;
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: 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;
@@ -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
  }
@@ -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
@@ -28,5 +28,5 @@
28
28
  "import": "./source/*.js"
29
29
  }
30
30
  },
31
- "version": "0.4.0"
31
+ "version": "0.4.1"
32
32
  }
@@ -2,7 +2,7 @@
2
2
  @typedef {object} NodeConfigurationOptions
3
3
  */
4
4
  /** @param {NodeConfigurationOptions} options */
5
- export function getNodeConfigOptions(options: NodeConfigurationOptions): {
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: NodeConfigurationOptions): Configuration;
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";
@@ -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
  }