gruber 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  This file documents notable changes to the project
4
4
 
5
+ ## 0.4.0
6
+
7
+ **new**
8
+
9
+ - Added `config.number(...)` & `config.boolean(...)` types along with `Structure` equivolents.
10
+ - Set a response body when creating a `HTTPError`, either via the constructor or the static methods.
11
+ - Set headers when creating an `HTTPError` and mutate the headers on it too, to be passed to the Response.
12
+ - 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.
13
+ - Added an unstable/experimental `Structure.array` for validating an array of a single Structure, e.g. an array of strings.
14
+ - Add number and boolean configurations (and their structures)
15
+
16
+ **fixes**
17
+
18
+ - Improve JSDoc types for Deno / Node clients
19
+ - Fix Structure typings
20
+ - Organise Config/Structure/Spec wording
21
+
5
22
  ## 0.3.0
6
23
 
7
24
  **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 _LiteralSpec {
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
+ variable?: string;
53
+ flag?: string;
54
+ fallback: any;
55
+ name: any;
56
+ type: 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,87 @@ 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
+ export class _LiteralSpec {
74
+ /**
75
+ * @param {string} type
76
+ * @param {SpecOptions<any>} options
77
+ */
78
+ constructor(type, options) {
79
+ this.type = type;
80
+ this.options = options;
81
+ }
82
+ describe(name) {
83
+ return {
84
+ fallback: this.options.fallback,
85
+ fields: [{ name, type: this.type, ...this.options }],
86
+ };
87
+ }
88
+ }
89
+
24
90
  const _requiredOptions = [
25
91
  "readTextFile",
26
92
  "getEnvironmentVariable",
@@ -29,6 +95,22 @@ const _requiredOptions = [
29
95
  "parse",
30
96
  ];
31
97
 
98
+ const _booleans = {
99
+ 1: true,
100
+ true: true,
101
+ 0: false,
102
+ false: false,
103
+ };
104
+
105
+ /**
106
+ * @typedef {object} ConfigurationOptions
107
+ * @property {(url: URL) => Promise<string | null>} readTextFile
108
+ * @property {(key: string) => (string | undefined)} getEnvironmentVariable
109
+ * @property {(key: string) => (string | undefined)} getCommandArgument
110
+ * @property {(value: any) => (string | Promise<string>)} stringify
111
+ * @property {(value: string) => (any)} parse
112
+ */
113
+
32
114
  export class Configuration {
33
115
  static spec = Symbol("Configuration.spec");
34
116
 
@@ -44,52 +126,128 @@ export class Configuration {
44
126
 
45
127
  /**
46
128
  * @template {Record<string, Structure<any>>} T
47
- * @param {T} spec
129
+ * @param {T} options
48
130
  * @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
49
131
  */
50
- object(spec) {
51
- const struct = Structure.object(spec);
52
- struct[Configuration.spec] = { type: "object", value: spec };
132
+ object(options) {
133
+ if (typeof options !== "object" || options === null) {
134
+ throw new TypeError("options must be a non-null object");
135
+ }
136
+ const struct = Structure.object(options);
137
+ struct[Configuration.spec] = new _ObjectSpec(options);
53
138
  return struct;
54
139
  }
55
140
 
56
141
  /**
57
- * @template {SpecOptions} Spec @param {Spec} spec
142
+ * @param {SpecOptions<string>} options
58
143
  * @returns {Structure<string>}
59
144
  */
60
- string(spec = {}) {
61
- if (typeof spec.fallback !== "string") {
62
- throw new TypeError("spec.fallback must be a string: " + spec.fallback);
145
+ string(options = {}) {
146
+ if (typeof options.fallback !== "string") {
147
+ throw new TypeError(
148
+ "options.fallback must be a string: " + options.fallback,
149
+ );
150
+ }
151
+
152
+ const struct = Structure.string(this._getValue(options).value);
153
+ struct[Configuration.spec] = new _LiteralSpec("string", options);
154
+ return struct;
155
+ }
156
+
157
+ /**
158
+ * @param {SpecOptions<number>} options
159
+ * @returns {Structure<number>}
160
+ */
161
+ number(options) {
162
+ if (typeof options.fallback !== "number") {
163
+ throw new TypeError("options.fallback must be a number");
164
+ }
165
+
166
+ const fallback = this._parseFloat(this._getValue(options));
167
+ const struct = Structure.number(fallback);
168
+ struct[Configuration.spec] = new _LiteralSpec("number", options);
169
+ return struct;
170
+ }
171
+
172
+ /**
173
+ * @param {SpecOptions<boolean>} options
174
+ * @returns {Structure<number>}
175
+ */
176
+ boolean(options) {
177
+ if (typeof options.fallback !== "boolean") {
178
+ throw new TypeError("options.fallback must be a boolean");
63
179
  }
64
- const struct = Structure.string(this._getValue(spec));
65
- struct[Configuration.spec] = { type: "string", value: spec };
180
+
181
+ const fallback = this._parseBoolean(this._getValue(options));
182
+ const struct = Structure.boolean(fallback);
183
+ struct[Configuration.spec] = new _LiteralSpec("boolean", options);
66
184
  return struct;
67
185
  }
68
186
 
69
187
  /**
70
- * @template {SpecOptions} Spec @param {Spec} spec
188
+ * @param {SpecOptions<string|URL>} options
71
189
  * @returns {Structure<URL>}
72
190
  */
73
- url(spec) {
74
- if (typeof spec.fallback !== "string") {
75
- throw new TypeError("spec.fallback must be a string");
191
+ url(options) {
192
+ if (
193
+ typeof options.fallback !== "string" &&
194
+ !(options.fallback instanceof URL)
195
+ ) {
196
+ throw new TypeError("options.fallback must be a string or URL");
76
197
  }
77
- const struct = Structure.url(this._getValue(spec));
78
- struct[Configuration.spec] = { type: "url", value: spec };
198
+ const struct = Structure.url(this._getValue(options).value);
199
+ struct[Configuration.spec] = new _LiteralSpec("url", {
200
+ ...options,
201
+ fallback: new URL(options.fallback),
202
+ });
79
203
  return struct;
80
204
  }
81
205
 
82
- /** @param {SpecOptions} spec */
83
- _getValue(spec) {
84
- const argument = spec.flag
85
- ? this.options.getCommandArgument(spec.flag)
206
+ /**
207
+ * @template T
208
+ * @param {SpecOptions<T>} options
209
+ * @returns {ConfigurationResult<T>}
210
+ */
211
+ _getValue(options) {
212
+ const argument = options.flag
213
+ ? this.options.getCommandArgument(options.flag)
86
214
  : null;
215
+ if (argument) return { source: "argument", value: argument };
87
216
 
88
- const variable = spec.variable
89
- ? this.options.getEnvironmentVariable(spec.variable)
217
+ const variable = options.variable
218
+ ? this.options.getEnvironmentVariable(options.variable)
90
219
  : null;
220
+ if (variable) return { source: "variable", value: variable };
221
+
222
+ return { source: "fallback", value: options.fallback };
223
+ }
91
224
 
92
- return argument ?? variable ?? spec.fallback;
225
+ /** @param {ConfigurationResult<number>} result */
226
+ _parseFloat(result) {
227
+ if (typeof result.value === "string") {
228
+ const parsed = Number.parseFloat(result.value);
229
+ if (Number.isNaN(parsed)) {
230
+ throw TypeError(`Invalid number: ${result.value}`);
231
+ }
232
+ return parsed;
233
+ }
234
+ if (typeof result.value === "number") {
235
+ return result.value;
236
+ }
237
+ throw new TypeError("Unknown result");
238
+ }
239
+
240
+ /** @param {ConfigurationResult<boolean>} result */
241
+ _parseBoolean(result) {
242
+ if (typeof result.value === "boolean") return result.value;
243
+
244
+ if (typeof _booleans[result.value] === "boolean") {
245
+ return _booleans[result.value];
246
+ }
247
+ if (result.source === "argument" && result.value === "") {
248
+ return true;
249
+ }
250
+ throw new TypeError("Unknown result");
93
251
  }
94
252
 
95
253
  /**
@@ -118,9 +276,9 @@ export class Configuration {
118
276
  }
119
277
  }
120
278
 
121
- /** @template T @param {T} config */
122
- getUsage(spec) {
123
- const { fallback, fields } = this.describeSpecification(spec);
279
+ /** @param {unknown} value */
280
+ getUsage(value) {
281
+ const { fallback, fields } = this.describe(value);
124
282
 
125
283
  const lines = [
126
284
  "Usage:",
@@ -140,46 +298,16 @@ export class Configuration {
140
298
  }
141
299
 
142
300
  /**
143
- * @template T @param {T} config
301
+ * @param {unknown} struct
144
302
  * @param {string} [prefix]
145
303
  * @returns {{ config: any, fields: [string, string] }}
146
304
  */
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 + "'");
305
+ describe(value, prefix = "") {
306
+ return _getSpec(prefix || ".", value).describe(prefix);
179
307
  }
180
308
 
181
- /** @param {Structure<any>} spec */
182
- getJSONSchema(spec) {
183
- return spec.getSchema();
309
+ /** * @param {Structure<any>} struct */
310
+ getJSONSchema(struct) {
311
+ return struct.getSchema();
184
312
  }
185
313
  }