gruber 0.2.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +156 -16
  3. package/core/configuration.d.ts +157 -0
  4. package/core/configuration.js +222 -96
  5. package/core/configuration.test.d.ts +1 -0
  6. package/core/configuration.test.js +242 -53
  7. package/{types/core → core}/fetch-router.d.ts +0 -1
  8. package/core/fetch-router.test.d.ts +1 -0
  9. package/{types/core → core}/http.d.ts +30 -12
  10. package/core/http.js +42 -17
  11. package/core/http.test.d.ts +1 -0
  12. package/core/http.test.js +57 -35
  13. package/{types/core → core}/migrator.d.ts +0 -1
  14. package/core/migrator.test.d.ts +1 -0
  15. package/{types/core → core}/mod.d.ts +1 -1
  16. package/core/mod.js +1 -0
  17. package/{types/core → core}/postgres.d.ts +0 -1
  18. package/core/structures.d.ts +91 -0
  19. package/core/structures.js +260 -0
  20. package/core/structures.test.d.ts +1 -0
  21. package/core/structures.test.js +474 -0
  22. package/core/test-deps.d.ts +1 -0
  23. package/core/test-deps.js +1 -1
  24. package/{types/core → core}/types.d.ts +0 -1
  25. package/{types/core → core}/utilities.d.ts +0 -1
  26. package/core/utilities.test.d.ts +1 -0
  27. package/package.json +4 -5
  28. package/{types/source → source}/configuration.d.ts +4 -9
  29. package/source/configuration.js +0 -2
  30. package/source/core.d.ts +1 -0
  31. package/{types/source → source}/express-router.d.ts +2 -3
  32. package/source/express-router.js +1 -1
  33. package/{types/source → source}/koa-router.d.ts +0 -1
  34. package/{types/source → source}/mod.d.ts +0 -3
  35. package/source/mod.js +2 -2
  36. package/{types/source → source}/node-router.d.ts +8 -8
  37. package/source/node-router.js +1 -1
  38. package/source/package-lock.json +3 -9
  39. package/source/package.json +0 -2
  40. package/source/polyfill.d.ts +1 -0
  41. package/{types/source → source}/postgres.d.ts +0 -1
  42. package/tsconfig.json +0 -2
  43. package/types/core/configuration.d.ts +0 -57
  44. package/types/core/configuration.d.ts.map +0 -1
  45. package/types/core/configuration.test.d.ts +0 -2
  46. package/types/core/configuration.test.d.ts.map +0 -1
  47. package/types/core/fetch-router.d.ts.map +0 -1
  48. package/types/core/fetch-router.test.d.ts +0 -2
  49. package/types/core/fetch-router.test.d.ts.map +0 -1
  50. package/types/core/http.d.ts.map +0 -1
  51. package/types/core/http.test.d.ts +0 -2
  52. package/types/core/http.test.d.ts.map +0 -1
  53. package/types/core/migrator.d.ts.map +0 -1
  54. package/types/core/migrator.test.d.ts +0 -2
  55. package/types/core/migrator.test.d.ts.map +0 -1
  56. package/types/core/mod.d.ts.map +0 -1
  57. package/types/core/postgres.d.ts.map +0 -1
  58. package/types/core/test-deps.d.ts +0 -2
  59. package/types/core/test-deps.d.ts.map +0 -1
  60. package/types/core/types.d.ts.map +0 -1
  61. package/types/core/utilities.d.ts.map +0 -1
  62. package/types/core/utilities.test.d.ts +0 -2
  63. package/types/core/utilities.test.d.ts.map +0 -1
  64. package/types/source/configuration.d.ts.map +0 -1
  65. package/types/source/core.d.ts +0 -2
  66. package/types/source/core.d.ts.map +0 -1
  67. package/types/source/express-router.d.ts.map +0 -1
  68. package/types/source/koa-router.d.ts.map +0 -1
  69. package/types/source/mod.d.ts.map +0 -1
  70. package/types/source/node-router.d.ts.map +0 -1
  71. package/types/source/polyfill.d.ts +0 -2
  72. package/types/source/polyfill.d.ts.map +0 -1
  73. package/types/source/postgres.d.ts.map +0 -1
@@ -1,24 +1,93 @@
1
1
  import { formatMarkdownTable } from "./utilities.js";
2
+ import { Structure, StructError } from "./structures.js";
3
+
4
+ // NOTE: it would be nice to reverse the object/string/url methods around so they return the "spec" value, then the "struct" is stored under a string. This could mean the underlying architecture could change in the future. I'm not sure if that is possible with the structure nesting in play.
5
+
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
2
7
 
3
8
  /**
9
+ * @template [T=any]
4
10
  * @typedef {object} SpecOptions
5
11
  * @property {string} [variable]
6
12
  * @property {string} [flag]
7
- * @property {string} fallback
13
+ * @property {T} fallback
8
14
  */
9
15
 
10
16
  /**
11
- * @typedef {object} ConfigurationOptions
12
- * @property {import("superstruct")} superstruct
13
- * @property {(url: URL) => Promise<string | null>} readTextFile
14
- * @property {(key: string) => (string | undefined)} getEnvironmentVariable
15
- * @property {(key: string) => (string | undefined)} getCommandArgument
16
- * @property {(value: any) => (string | Promise<string>)} stringify
17
- * @property {(value: string) => (any)} parse
17
+ * @template T
18
+ * @typedef {object} ConfigurationResult
19
+ * @property {'argument' | 'variable' | 'fallback'} source
20
+ * @property {string|T} value
21
+ */
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}
18
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
+ }
19
89
 
20
90
  const _requiredOptions = [
21
- "superstruct",
22
91
  "readTextFile",
23
92
  "getEnvironmentVariable",
24
93
  "getCommandArgument",
@@ -26,12 +95,26 @@ const _requiredOptions = [
26
95
  "parse",
27
96
  ];
28
97
 
29
- /** @typedef {Record<string, import("superstruct").Struct<any, any>>} ObjectSchema */
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
+ */
30
113
 
31
114
  export class Configuration {
32
115
  static spec = Symbol("Configuration.spec");
33
116
 
34
- options /** @type {ConfigurationOptions} */;
117
+ /** @type {ConfigurationOptions} */ options;
35
118
 
36
119
  /** @param {ConfigurationOptions} options */
37
120
  constructor(options) {
@@ -42,71 +125,135 @@ export class Configuration {
42
125
  }
43
126
 
44
127
  /**
45
- * @template {ObjectSchema} T
46
- * @param {T} spec
128
+ * @template {Record<string, Structure<any>>} T
129
+ * @param {T} options
130
+ * @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
47
131
  */
48
- object(spec) {
49
- const struct = this.options.superstruct.defaulted(
50
- this.options.superstruct.object(spec),
51
- {},
52
- )
53
- struct[Configuration.spec]= { type: "object", value: spec }
54
- return struct
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);
138
+ return struct;
55
139
  }
56
140
 
57
141
  /**
58
- * @template {SpecOptions} Spec @param {Spec} spec
59
- * @returns {import("superstruct").Struct<string, null>}
142
+ * @param {SpecOptions<string>} options
143
+ * @returns {Structure<string>}
60
144
  */
61
- string(spec = {}) {
62
- if (typeof spec.fallback !== "string") {
63
- 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
+ );
64
150
  }
65
- const struct = this.options.superstruct.defaulted(
66
- this.options.superstruct.string(),
67
- this._getValue(spec),
68
- )
69
- struct[Configuration.spec] = { type: "string", value: spec }
70
- return struct
151
+
152
+ const struct = Structure.string(this._getValue(options).value);
153
+ struct[Configuration.spec] = new _LiteralSpec("string", options);
154
+ return struct;
71
155
  }
72
156
 
73
157
  /**
74
- * @template {SpecOptions} Spec @param {Spec} spec
75
- * @returns {import("superstruct").Struct<URL, null>}
158
+ * @param {SpecOptions<number>} options
159
+ * @returns {Structure<number>}
76
160
  */
77
- url(spec) {
78
- if (typeof spec.fallback !== "string") {
79
- throw new TypeError("spec.fallback must be a string");
161
+ number(options) {
162
+ if (typeof options.fallback !== "number") {
163
+ throw new TypeError("options.fallback must be a number");
80
164
  }
81
- const struct = this.options.superstruct.defaulted(
82
- this.options.superstruct.coerce(
83
- this.options.superstruct.instance(URL),
84
- this.options.superstruct.string(),
85
- (value) => new URL(value),
86
- ),
87
- this._getValue(spec),
88
- )
89
- struct[Configuration.spec]= { type: "url", value: spec }
90
- return struct
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");
179
+ }
180
+
181
+ const fallback = this._parseBoolean(this._getValue(options));
182
+ const struct = Structure.boolean(fallback);
183
+ struct[Configuration.spec] = new _LiteralSpec("boolean", options);
184
+ return struct;
185
+ }
186
+
187
+ /**
188
+ * @param {SpecOptions<string|URL>} options
189
+ * @returns {Structure<URL>}
190
+ */
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");
197
+ }
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
+ });
203
+ return struct;
91
204
  }
92
205
 
93
- /** @param {SpecOptions} spec */
94
- _getValue(spec) {
95
- const argument = spec.flag
96
- ? 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)
97
214
  : null;
215
+ if (argument) return { source: "argument", value: argument };
98
216
 
99
- const variable = spec.variable
100
- ? this.options.getEnvironmentVariable(spec.variable)
217
+ const variable = options.variable
218
+ ? this.options.getEnvironmentVariable(options.variable)
101
219
  : null;
220
+ if (variable) return { source: "variable", value: variable };
221
+
222
+ return { source: "fallback", value: options.fallback };
223
+ }
224
+
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
+ }
102
239
 
103
- return argument ?? variable ?? spec.fallback;
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");
104
251
  }
105
252
 
106
253
  /**
107
254
  * @template T
108
255
  * @param {URL} url
109
- * @param {import("superstruct").Struct<T>} spec
256
+ * @param {Structure<T>} spec
110
257
  * @returns {Promise<T>}
111
258
  */
112
259
  async load(url, spec) {
@@ -114,20 +261,24 @@ export class Configuration {
114
261
 
115
262
  // Catch missing files and create a default configuration
116
263
  if (!file) {
117
- return this.options.superstruct.create({}, spec);
264
+ return spec.process({});
118
265
  }
119
266
 
120
267
  // Fail outside the try-catch to surface structure errors
121
- return this.options.superstruct.create(
122
- await this.options.parse(file),
123
- spec,
124
- "Configuration failed to parse",
125
- );
268
+ try {
269
+ return spec.process(await this.options.parse(file));
270
+ } catch (error) {
271
+ console.error("Configuration failed to parse");
272
+ if (error instanceof StructError) {
273
+ error.message = error.toFriendlyString();
274
+ }
275
+ throw error;
276
+ }
126
277
  }
127
278
 
128
- /** @template T @param {T} config */
129
- getUsage(spec) {
130
- const { fallback, fields } = this.describeSpecification(spec);
279
+ /** @param {unknown} value */
280
+ getUsage(value) {
281
+ const { fallback, fields } = this.describe(value);
131
282
 
132
283
  const lines = [
133
284
  "Usage:",
@@ -147,41 +298,16 @@ export class Configuration {
147
298
  }
148
299
 
149
300
  /**
150
- * @template T @param {T} config
301
+ * @param {unknown} struct
151
302
  * @param {string} [prefix]
152
303
  * @returns {{ config: any, fields: [string, string] }}
153
304
  */
154
- describeSpecification(spec, prefix = "") {
155
- if (!spec[Configuration.spec]) {
156
- throw new TypeError("Invalid [Configuration.spec]");
157
- }
158
- const { type, value } = spec[Configuration.spec];
159
-
160
- if (type === "object") {
161
- const fallback = {};
162
- const fields = [];
163
- for (const [key, value2] of Object.entries(value)) {
164
- const child = this.describeSpecification(
165
- value2,
166
- (prefix ? prefix + "." : "") + key,
167
- );
168
- fallback[key] = child.fallback;
169
- fields.push(...child.fields);
170
- }
171
- return { fallback, fields };
172
- }
173
- if (type === "string") {
174
- return {
175
- fallback: value.fallback,
176
- fields: [{ name: prefix, type, ...value }],
177
- };
178
- }
179
- if (type === "url") {
180
- return {
181
- fallback: new URL(value.fallback),
182
- fields: [{ name: prefix, type, ...value }],
183
- };
184
- }
185
- throw new TypeError("Invalid [Configuration.spec].type '" + type + "'");
305
+ describe(value, prefix = "") {
306
+ return _getSpec(prefix || ".", value).describe(prefix);
307
+ }
308
+
309
+ /** * @param {Structure<any>} struct */
310
+ getJSONSchema(struct) {
311
+ return struct.getSchema();
186
312
  }
187
313
  }
@@ -0,0 +1 @@
1
+ export {};