gruber 0.3.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,33 @@
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
+
15
+ ## 0.4.0
16
+
17
+ **new**
18
+
19
+ - Added `config.number(...)` & `config.boolean(...)` types along with `Structure` equivolents.
20
+ - Set a response body when creating a `HTTPError`, either via the constructor or the static methods.
21
+ - Set headers when creating an `HTTPError` and mutate the headers on it too, to be passed to the Response.
22
+ - Structure primatives' fallback is now optional. If a fallback isn't provided, validation will fail if with a "Missing value" if no value is provided.
23
+ - Added an unstable/experimental `Structure.array` for validating an array of a single Structure, e.g. an array of strings.
24
+ - Add number and boolean configurations (and their structures)
25
+
26
+ **fixes**
27
+
28
+ - Improve JSDoc types for Deno / Node clients
29
+ - Fix Structure typings
30
+ - Organise Config/Structure/Spec wording
31
+
5
32
  ## 0.3.0
6
33
 
7
34
  **new**
package/README.md CHANGED
@@ -241,11 +241,14 @@ import { getNodeConfiguration } from "gruber";
241
241
  const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
242
242
  const config = getNodeConfiguration();
243
243
 
244
- export function getSpecification() {
244
+ export function getConfigStruct() {
245
245
  return config.object({
246
- env: config.string({
247
- variable: "NODE_ENV",
248
- fallback: "development",
246
+ env: config.string({ variable: "NODE_ENV", fallback: "development" }),
247
+
248
+ port: config.number({
249
+ variable: "APP_PORT",
250
+ flag: "--port",
251
+ fallback: 8000,
249
252
  }),
250
253
 
251
254
  selfUrl: config.url({
@@ -253,13 +256,13 @@ export function getSpecification() {
253
256
  fallback: "http://localhost:3000",
254
257
  }),
255
258
 
256
- // Short hands?
257
259
  meta: config.object({
258
260
  name: config.string({ flag: "--app-name", fallback: pkg.name }),
259
261
  version: config.string({ fallback: pkg.version }),
260
262
  }),
261
263
 
262
264
  database: config.object({
265
+ useSsl: config.boolean({ flag: "--database-ssl", fallback: true }),
263
266
  url: config.url({
264
267
  variable: "DATABASE_URL",
265
268
  flag: "--database-url",
@@ -271,11 +274,11 @@ export function getSpecification() {
271
274
 
272
275
  // Load the configuration and parse it
273
276
  export function loadConfiguration(path) {
274
- return config.load(path, getSpecification());
277
+ return config.load(path, getConfigStruct());
275
278
  }
276
279
 
277
280
  // TypeScript thought:
278
- // export type Configuration = Infer<ReturnType<typeof getSpecification>>
281
+ // export type Configuration = Infer<ReturnType<typeof getConfigStruct>>
279
282
 
280
283
  // Expose the configutation for use in the application
281
284
  export const appConfig = await loadConfiguration(
@@ -284,12 +287,12 @@ export const appConfig = await loadConfiguration(
284
287
 
285
288
  // Export a method to generate usage documentation
286
289
  export function getConfigurationUsage() {
287
- return config.getUsage(getSpecification());
290
+ return config.getUsage(getConfigStruct());
288
291
  }
289
292
 
290
293
  // Export a method to generate a JSON Schema for the configuration
291
294
  export function getConfigurationSchema() {
292
- return config.getJSONSchema(getSpecification());
295
+ return config.getJSONSchema(getConfigStruct());
293
296
  }
294
297
  ```
295
298
 
@@ -332,11 +335,11 @@ You can provide a configuration file like **config.json** to load through the co
332
335
  "selfUrl": "http://localhost:3000",
333
336
  "meta": {
334
337
  "name": "gruber-app",
335
- "version": "1.2.3"
338
+ "version": "1.2.3",
336
339
  },
337
340
  "database": {
338
- "url": "postgres://user:secret@localhost:5432/database"
339
- }
341
+ "url": "postgres://user:secret@localhost:5432/database",
342
+ },
340
343
  }
341
344
  ```
342
345
 
@@ -362,7 +365,7 @@ this can be done like so:
362
365
 
363
366
  ```js
364
367
  export function loadConfiguration() {
365
- const appConfig = config.loadJsonSync(path, getSpecification());
368
+ const appConfig = config.loadJsonSync(path, getConfigStruct());
366
369
 
367
370
  // Only run these checks when running in production
368
371
  if (appConfig.env === "production") {
@@ -808,6 +811,8 @@ more can be added in the future as the need arrises.
808
811
  They directly map to HTTP error as codes documented on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
809
812
 
810
813
  ```js
814
+ import { HTTPError } from "gruber";
815
+
811
816
  const teapot = new HTTPError(418, "I'm a teapot");
812
817
  ```
813
818
 
@@ -821,6 +826,55 @@ teapot.toResponse();
821
826
  Currently, you can't set the body of the generated Response objects.
822
827
  This would be nice to have in the future, but the API should be thoughtfully designed first.
823
828
 
829
+ **Request body**
830
+
831
+ You can set the body to be returned when the HTTPError is thrown from the constructor or the factory methods:
832
+
833
+ ```ts
834
+ import { HTTPError } from "gruber";
835
+
836
+ const teapot = new HTTPError(418, "I'm a teapot", "model=teabot-5000");
837
+
838
+ throw HTTPError.badRequest("no coffee provided");
839
+ ```
840
+
841
+ The value of the body is the same as the `body` in the
842
+ [Response constructor](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#body).
843
+
844
+ **Headers**
845
+
846
+ > _EXPERIMENTAL_
847
+
848
+ If you really want, you can set headers on a HTTPError too:
849
+
850
+ ```ts
851
+ import { HTTPError } from "gruber";
852
+
853
+ const teapot = new HTTPError(
854
+ 400,
855
+ "Bad Request",
856
+ JSON.stringify({ some: "thing" }),
857
+ { "Content-Type": "application/json" },
858
+ );
859
+
860
+ // or via mutating the headers object
861
+ teapot.headers.set("X-HOTEL-BAR", "Hotel Bar?");
862
+ ```
863
+
864
+ If you want fine-grain control, you might be better off creating a subclass, e.g. `BadJSONRequest`:
865
+
866
+ ```ts
867
+ class BadJSONRequest extends HTTPError {
868
+ constructor(body) {
869
+ super(400, "Bad Request", body, { "Content-type": "application/json" });
870
+ this.name = "BadJSONRequest";
871
+ Error.captureStackTrace(this, BadJSONRequest);
872
+ }
873
+ }
874
+
875
+ throw new BadJSONRequest({ message: "Something went wrong..." });
876
+ ```
877
+
824
878
  ### FetchRouter
825
879
 
826
880
  `FetchRouter` is a web-native router for routes defined with `defineRoute`.
@@ -1,3 +1,70 @@
1
+ /**
2
+ * @template [T=any]
3
+ * @typedef {object} SpecOptions
4
+ * @property {string} [variable]
5
+ * @property {string} [flag]
6
+ * @property {T} fallback
7
+ */
8
+ /**
9
+ * @template T
10
+ * @typedef {object} ConfigurationResult
11
+ * @property {'argument' | 'variable' | 'fallback'} source
12
+ * @property {string|T} value
13
+ */
14
+ /**
15
+ * @typedef {object} ConfigurationDescription
16
+ * @property {unknown} fallback
17
+ * @property {Record<string,string>[]} fields
18
+ */
19
+ /**
20
+ * @typedef {object} Specification
21
+ * @property {string} type
22
+ * @property {any} options
23
+ * @property {(name: string) => ConfigurationDescription} describe
24
+ */
25
+ /**
26
+ * @param {string} name
27
+ * @param {unknown} value
28
+ * @returns {Specification}
29
+ */
30
+ export function _getSpec(name: string, value: unknown): Specification;
31
+ export class _ObjectSpec {
32
+ /** @param {Record<string, SpecOptions>} options */
33
+ constructor(options: Record<string, SpecOptions>);
34
+ type: string;
35
+ options: Record<string, SpecOptions<any>>;
36
+ describe(name: any): {
37
+ fallback: {};
38
+ fields: Record<string, string>[];
39
+ };
40
+ }
41
+ export class _PrimativeSpec {
42
+ /**
43
+ * @param {string} type
44
+ * @param {SpecOptions<any>} options
45
+ */
46
+ constructor(type: string, options: SpecOptions<any>);
47
+ type: string;
48
+ options: SpecOptions<any>;
49
+ describe(name: any): {
50
+ fallback: any;
51
+ fields: {
52
+ name: any;
53
+ type: string;
54
+ fallback: any;
55
+ variable?: string;
56
+ flag?: string;
57
+ }[];
58
+ };
59
+ }
60
+ /**
61
+ * @typedef {object} ConfigurationOptions
62
+ * @property {(url: URL) => Promise<string | null>} readTextFile
63
+ * @property {(key: string) => (string | undefined)} getEnvironmentVariable
64
+ * @property {(key: string) => (string | undefined)} getCommandArgument
65
+ * @property {(value: any) => (string | Promise<string>)} stringify
66
+ * @property {(value: string) => (any)} parse
67
+ */
1
68
  export class Configuration {
2
69
  static spec: symbol;
3
70
  /** @param {ConfigurationOptions} options */
@@ -5,49 +72,80 @@ export class Configuration {
5
72
  /** @type {ConfigurationOptions} */ options: ConfigurationOptions;
6
73
  /**
7
74
  * @template {Record<string, Structure<any>>} T
8
- * @param {T} spec
75
+ * @param {T} options
9
76
  * @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
10
77
  */
11
- object<T extends Record<string, Structure<any>>>(spec: T): Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]>; }>;
78
+ object<T extends Record<string, Structure<any>>>(options: T): Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]>; }>;
12
79
  /**
13
- * @template {SpecOptions} Spec @param {Spec} spec
80
+ * @param {SpecOptions<string>} options
14
81
  * @returns {Structure<string>}
15
82
  */
16
- string<Spec extends SpecOptions>(spec?: Spec): Structure<string>;
83
+ string(options?: SpecOptions<string>): Structure<string>;
84
+ /**
85
+ * @param {SpecOptions<number>} options
86
+ * @returns {Structure<number>}
87
+ */
88
+ number(options: SpecOptions<number>): Structure<number>;
17
89
  /**
18
- * @template {SpecOptions} Spec @param {Spec} spec
90
+ * @param {SpecOptions<boolean>} options
91
+ * @returns {Structure<number>}
92
+ */
93
+ boolean(options: SpecOptions<boolean>): Structure<number>;
94
+ /**
95
+ * @param {SpecOptions<string|URL>} options
19
96
  * @returns {Structure<URL>}
20
97
  */
21
- url<Spec_1 extends SpecOptions>(spec: Spec_1): Structure<URL>;
22
- /** @param {SpecOptions} spec */
23
- _getValue(spec: SpecOptions): string;
98
+ url(options: SpecOptions<string | URL>): Structure<URL>;
99
+ /**
100
+ * @template T
101
+ * @param {SpecOptions<T>} options
102
+ * @returns {ConfigurationResult<T>}
103
+ */
104
+ _getValue<T_1>(options: SpecOptions<T_1>): ConfigurationResult<T_1>;
105
+ /** @param {ConfigurationResult<number>} result */
106
+ _parseFloat(result: ConfigurationResult<number>): number;
107
+ /** @param {ConfigurationResult<boolean>} result */
108
+ _parseBoolean(result: ConfigurationResult<boolean>): any;
24
109
  /**
25
110
  * @template T
26
111
  * @param {URL} url
27
112
  * @param {Structure<T>} spec
28
113
  * @returns {Promise<T>}
29
114
  */
30
- load<T_1>(url: URL, spec: Structure<T_1>): Promise<T_1>;
31
- /** @template T @param {T} config */
32
- getUsage<T_2>(spec: any): string;
115
+ load<T_2>(url: URL, spec: Structure<T_2>): Promise<T_2>;
116
+ /** @param {unknown} value */
117
+ getUsage(value: unknown): string;
33
118
  /**
34
- * @template T @param {T} config
119
+ * @param {unknown} struct
35
120
  * @param {string} [prefix]
36
121
  * @returns {{ config: any, fields: [string, string] }}
37
122
  */
38
- describeSpecification<T_3>(spec: any, prefix?: string): {
123
+ describe(value: any, prefix?: string): {
39
124
  config: any;
40
125
  fields: [string, string];
41
126
  };
42
- /** @param {Structure<any>} spec */
43
- getJSONSchema(spec: Structure<any>): {
127
+ /** * @param {Structure<any>} struct */
128
+ getJSONSchema(struct: Structure<any>): {
44
129
  $schema: string;
45
130
  };
46
131
  }
47
- export type SpecOptions = {
132
+ export type SpecOptions<T = any> = {
48
133
  variable?: string;
49
134
  flag?: string;
50
- fallback: string;
135
+ fallback: T;
136
+ };
137
+ export type ConfigurationResult<T> = {
138
+ source: 'argument' | 'variable' | 'fallback';
139
+ value: string | T;
140
+ };
141
+ export type ConfigurationDescription = {
142
+ fallback: unknown;
143
+ fields: Record<string, string>[];
144
+ };
145
+ export type Specification = {
146
+ type: string;
147
+ options: any;
148
+ describe: (name: string) => ConfigurationDescription;
51
149
  };
52
150
  export type ConfigurationOptions = {
53
151
  readTextFile: (url: URL) => Promise<string | null>;
@@ -6,21 +6,98 @@ import { Structure, StructError } from "./structures.js";
6
6
  // NOTE: the schema generation will include whatever value is passed to the structure, in the context of configuration it will be whatever is configured and may be something secret
7
7
 
8
8
  /**
9
+ * @template [T=any]
9
10
  * @typedef {object} SpecOptions
10
11
  * @property {string} [variable]
11
12
  * @property {string} [flag]
12
- * @property {string} fallback
13
+ * @property {T} fallback
13
14
  */
14
15
 
15
16
  /**
16
- * @typedef {object} ConfigurationOptions
17
- * @property {(url: URL) => Promise<string | null>} readTextFile
18
- * @property {(key: string) => (string | undefined)} getEnvironmentVariable
19
- * @property {(key: string) => (string | undefined)} getCommandArgument
20
- * @property {(value: any) => (string | Promise<string>)} stringify
21
- * @property {(value: string) => (any)} parse
17
+ * @template T
18
+ * @typedef {object} ConfigurationResult
19
+ * @property {'argument' | 'variable' | 'fallback'} source
20
+ * @property {string|T} value
22
21
  */
23
22
 
23
+ /**
24
+ * @typedef {object} ConfigurationDescription
25
+ * @property {unknown} fallback
26
+ * @property {Record<string,string>[]} fields
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} Specification
31
+ * @property {string} type
32
+ * @property {any} options
33
+ * @property {(name: string) => ConfigurationDescription} describe
34
+ */
35
+
36
+ /**
37
+ * @param {string} name
38
+ * @param {unknown} value
39
+ * @returns {Specification}
40
+ */
41
+ export function _getSpec(name, value) {
42
+ if (
43
+ typeof value[Configuration.spec] !== "object" ||
44
+ typeof value[Configuration.spec].type !== "string" ||
45
+ typeof value[Configuration.spec].options !== "object" ||
46
+ typeof value[Configuration.spec].describe !== "function"
47
+ ) {
48
+ throw new TypeError(`Invalid [Configuration.spec] for '${name}'`);
49
+ }
50
+ return value[Configuration.spec];
51
+ }
52
+
53
+ export class _ObjectSpec {
54
+ /** @param {Record<string, SpecOptions>} options */
55
+ constructor(options) {
56
+ this.type = "object";
57
+ this.options = options;
58
+ }
59
+ describe(name) {
60
+ const fallback = {};
61
+ const fields = [];
62
+ for (const [key, childOptions] of Object.entries(this.options)) {
63
+ const childName = (name ? name + "." : "") + key;
64
+ const childSpec = _getSpec(childName, childOptions).describe(childName);
65
+
66
+ fallback[key] = childSpec.fallback;
67
+ fields.push(...childSpec.fields);
68
+ }
69
+ return { fallback, fields };
70
+ }
71
+ }
72
+
73
+ //
74
+ // NOTE: describe() calls should return the actual value in "fallback"
75
+ // and the string-value in fields
76
+ //
77
+ export class _PrimativeSpec {
78
+ /**
79
+ * @param {string} type
80
+ * @param {SpecOptions<any>} options
81
+ */
82
+ constructor(type, options) {
83
+ this.type = type;
84
+ this.options = options;
85
+ }
86
+ describe(name) {
87
+ return {
88
+ fallback: this.options.fallback,
89
+ fields: [
90
+ {
91
+ ...this.options,
92
+ name,
93
+ type: this.type,
94
+ fallback: this.options.fallback?.toString(),
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ }
100
+
24
101
  const _requiredOptions = [
25
102
  "readTextFile",
26
103
  "getEnvironmentVariable",
@@ -29,6 +106,22 @@ const _requiredOptions = [
29
106
  "parse",
30
107
  ];
31
108
 
109
+ const _booleans = {
110
+ 1: true,
111
+ true: true,
112
+ 0: false,
113
+ false: false,
114
+ };
115
+
116
+ /**
117
+ * @typedef {object} ConfigurationOptions
118
+ * @property {(url: URL) => Promise<string | null>} readTextFile
119
+ * @property {(key: string) => (string | undefined)} getEnvironmentVariable
120
+ * @property {(key: string) => (string | undefined)} getCommandArgument
121
+ * @property {(value: any) => (string | Promise<string>)} stringify
122
+ * @property {(value: string) => (any)} parse
123
+ */
124
+
32
125
  export class Configuration {
33
126
  static spec = Symbol("Configuration.spec");
34
127
 
@@ -44,52 +137,128 @@ export class Configuration {
44
137
 
45
138
  /**
46
139
  * @template {Record<string, Structure<any>>} T
47
- * @param {T} spec
140
+ * @param {T} options
48
141
  * @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
49
142
  */
50
- object(spec) {
51
- const struct = Structure.object(spec);
52
- struct[Configuration.spec] = { type: "object", value: spec };
143
+ object(options) {
144
+ if (typeof options !== "object" || options === null) {
145
+ throw new TypeError("options must be a non-null object");
146
+ }
147
+ const struct = Structure.object(options);
148
+ struct[Configuration.spec] = new _ObjectSpec(options);
53
149
  return struct;
54
150
  }
55
151
 
56
152
  /**
57
- * @template {SpecOptions} Spec @param {Spec} spec
153
+ * @param {SpecOptions<string>} options
58
154
  * @returns {Structure<string>}
59
155
  */
60
- string(spec = {}) {
61
- if (typeof spec.fallback !== "string") {
62
- throw new TypeError("spec.fallback must be a string: " + spec.fallback);
156
+ string(options = {}) {
157
+ if (typeof options.fallback !== "string") {
158
+ throw new TypeError(
159
+ "options.fallback must be a string: " + options.fallback,
160
+ );
161
+ }
162
+
163
+ const struct = Structure.string(this._getValue(options).value);
164
+ struct[Configuration.spec] = new _PrimativeSpec("string", options);
165
+ return struct;
166
+ }
167
+
168
+ /**
169
+ * @param {SpecOptions<number>} options
170
+ * @returns {Structure<number>}
171
+ */
172
+ number(options) {
173
+ if (typeof options.fallback !== "number") {
174
+ throw new TypeError("options.fallback must be a number");
175
+ }
176
+
177
+ const fallback = this._parseFloat(this._getValue(options));
178
+ const struct = Structure.number(fallback);
179
+ struct[Configuration.spec] = new _PrimativeSpec("number", options);
180
+ return struct;
181
+ }
182
+
183
+ /**
184
+ * @param {SpecOptions<boolean>} options
185
+ * @returns {Structure<number>}
186
+ */
187
+ boolean(options) {
188
+ if (typeof options.fallback !== "boolean") {
189
+ throw new TypeError("options.fallback must be a boolean");
63
190
  }
64
- const struct = Structure.string(this._getValue(spec));
65
- struct[Configuration.spec] = { type: "string", value: spec };
191
+
192
+ const fallback = this._parseBoolean(this._getValue(options));
193
+ const struct = Structure.boolean(fallback);
194
+ struct[Configuration.spec] = new _PrimativeSpec("boolean", options);
66
195
  return struct;
67
196
  }
68
197
 
69
198
  /**
70
- * @template {SpecOptions} Spec @param {Spec} spec
199
+ * @param {SpecOptions<string|URL>} options
71
200
  * @returns {Structure<URL>}
72
201
  */
73
- url(spec) {
74
- if (typeof spec.fallback !== "string") {
75
- throw new TypeError("spec.fallback must be a string");
202
+ url(options) {
203
+ if (
204
+ typeof options.fallback !== "string" &&
205
+ !(options.fallback instanceof URL)
206
+ ) {
207
+ throw new TypeError("options.fallback must be a string or URL");
76
208
  }
77
- const struct = Structure.url(this._getValue(spec));
78
- struct[Configuration.spec] = { type: "url", value: spec };
209
+ const struct = Structure.url(this._getValue(options).value);
210
+ struct[Configuration.spec] = new _PrimativeSpec("url", {
211
+ ...options,
212
+ fallback: new URL(options.fallback),
213
+ });
79
214
  return struct;
80
215
  }
81
216
 
82
- /** @param {SpecOptions} spec */
83
- _getValue(spec) {
84
- const argument = spec.flag
85
- ? this.options.getCommandArgument(spec.flag)
217
+ /**
218
+ * @template T
219
+ * @param {SpecOptions<T>} options
220
+ * @returns {ConfigurationResult<T>}
221
+ */
222
+ _getValue(options) {
223
+ const argument = options.flag
224
+ ? this.options.getCommandArgument(options.flag)
86
225
  : null;
226
+ if (argument) return { source: "argument", value: argument };
87
227
 
88
- const variable = spec.variable
89
- ? this.options.getEnvironmentVariable(spec.variable)
228
+ const variable = options.variable
229
+ ? this.options.getEnvironmentVariable(options.variable)
90
230
  : null;
231
+ if (variable) return { source: "variable", value: variable };
232
+
233
+ return { source: "fallback", value: options.fallback };
234
+ }
91
235
 
92
- return argument ?? variable ?? spec.fallback;
236
+ /** @param {ConfigurationResult<number>} result */
237
+ _parseFloat(result) {
238
+ if (typeof result.value === "string") {
239
+ const parsed = Number.parseFloat(result.value);
240
+ if (Number.isNaN(parsed)) {
241
+ throw TypeError(`Invalid number: ${result.value}`);
242
+ }
243
+ return parsed;
244
+ }
245
+ if (typeof result.value === "number") {
246
+ return result.value;
247
+ }
248
+ throw new TypeError("Unknown result");
249
+ }
250
+
251
+ /** @param {ConfigurationResult<boolean>} result */
252
+ _parseBoolean(result) {
253
+ if (typeof result.value === "boolean") return result.value;
254
+
255
+ if (typeof _booleans[result.value] === "boolean") {
256
+ return _booleans[result.value];
257
+ }
258
+ if (result.source === "argument" && result.value === "") {
259
+ return true;
260
+ }
261
+ throw new TypeError("Unknown result");
93
262
  }
94
263
 
95
264
  /**
@@ -118,9 +287,9 @@ export class Configuration {
118
287
  }
119
288
  }
120
289
 
121
- /** @template T @param {T} config */
122
- getUsage(spec) {
123
- const { fallback, fields } = this.describeSpecification(spec);
290
+ /** @param {unknown} value */
291
+ getUsage(value) {
292
+ const { fallback, fields } = this.describe(value);
124
293
 
125
294
  const lines = [
126
295
  "Usage:",
@@ -140,46 +309,16 @@ export class Configuration {
140
309
  }
141
310
 
142
311
  /**
143
- * @template T @param {T} config
312
+ * @param {unknown} struct
144
313
  * @param {string} [prefix]
145
314
  * @returns {{ config: any, fields: [string, string] }}
146
315
  */
147
- describeSpecification(spec, prefix = "") {
148
- if (!spec[Configuration.spec]) {
149
- throw new TypeError("Invalid [Configuration.spec]");
150
- }
151
- const { type, value } = spec[Configuration.spec];
152
-
153
- if (type === "object") {
154
- const fallback = {};
155
- const fields = [];
156
- for (const [key, value2] of Object.entries(value)) {
157
- const child = this.describeSpecification(
158
- value2,
159
- (prefix ? prefix + "." : "") + key,
160
- );
161
- fallback[key] = child.fallback;
162
- fields.push(...child.fields);
163
- }
164
- return { fallback, fields };
165
- }
166
- if (type === "string") {
167
- return {
168
- fallback: value.fallback,
169
- fields: [{ name: prefix, type, ...value }],
170
- };
171
- }
172
- if (type === "url") {
173
- return {
174
- fallback: new URL(value.fallback),
175
- fields: [{ name: prefix, type, ...value }],
176
- };
177
- }
178
- throw new TypeError("Invalid [Configuration.spec].type '" + type + "'");
316
+ describe(value, prefix = "") {
317
+ return _getSpec(prefix || ".", value).describe(prefix);
179
318
  }
180
319
 
181
- /** @param {Structure<any>} spec */
182
- getJSONSchema(spec) {
183
- return spec.getSchema();
320
+ /** * @param {Structure<any>} struct */
321
+ getJSONSchema(struct) {
322
+ return struct.getSchema();
184
323
  }
185
324
  }