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.
- package/CHANGELOG.md +29 -0
- package/README.md +156 -16
- package/core/configuration.d.ts +157 -0
- package/core/configuration.js +222 -96
- package/core/configuration.test.d.ts +1 -0
- package/core/configuration.test.js +242 -53
- package/{types/core → core}/fetch-router.d.ts +0 -1
- package/core/fetch-router.test.d.ts +1 -0
- package/{types/core → core}/http.d.ts +30 -12
- package/core/http.js +42 -17
- package/core/http.test.d.ts +1 -0
- package/core/http.test.js +57 -35
- package/{types/core → core}/migrator.d.ts +0 -1
- package/core/migrator.test.d.ts +1 -0
- package/{types/core → core}/mod.d.ts +1 -1
- package/core/mod.js +1 -0
- package/{types/core → core}/postgres.d.ts +0 -1
- package/core/structures.d.ts +91 -0
- package/core/structures.js +260 -0
- package/core/structures.test.d.ts +1 -0
- package/core/structures.test.js +474 -0
- package/core/test-deps.d.ts +1 -0
- package/core/test-deps.js +1 -1
- package/{types/core → core}/types.d.ts +0 -1
- package/{types/core → core}/utilities.d.ts +0 -1
- package/core/utilities.test.d.ts +1 -0
- package/package.json +4 -5
- package/{types/source → source}/configuration.d.ts +4 -9
- package/source/configuration.js +0 -2
- package/source/core.d.ts +1 -0
- package/{types/source → source}/express-router.d.ts +2 -3
- package/source/express-router.js +1 -1
- package/{types/source → source}/koa-router.d.ts +0 -1
- package/{types/source → source}/mod.d.ts +0 -3
- package/source/mod.js +2 -2
- package/{types/source → source}/node-router.d.ts +8 -8
- package/source/node-router.js +1 -1
- package/source/package-lock.json +3 -9
- package/source/package.json +0 -2
- package/source/polyfill.d.ts +1 -0
- package/{types/source → source}/postgres.d.ts +0 -1
- package/tsconfig.json +0 -2
- package/types/core/configuration.d.ts +0 -57
- package/types/core/configuration.d.ts.map +0 -1
- package/types/core/configuration.test.d.ts +0 -2
- package/types/core/configuration.test.d.ts.map +0 -1
- package/types/core/fetch-router.d.ts.map +0 -1
- package/types/core/fetch-router.test.d.ts +0 -2
- package/types/core/fetch-router.test.d.ts.map +0 -1
- package/types/core/http.d.ts.map +0 -1
- package/types/core/http.test.d.ts +0 -2
- package/types/core/http.test.d.ts.map +0 -1
- package/types/core/migrator.d.ts.map +0 -1
- package/types/core/migrator.test.d.ts +0 -2
- package/types/core/migrator.test.d.ts.map +0 -1
- package/types/core/mod.d.ts.map +0 -1
- package/types/core/postgres.d.ts.map +0 -1
- package/types/core/test-deps.d.ts +0 -2
- package/types/core/test-deps.d.ts.map +0 -1
- package/types/core/types.d.ts.map +0 -1
- package/types/core/utilities.d.ts.map +0 -1
- package/types/core/utilities.test.d.ts +0 -2
- package/types/core/utilities.test.d.ts.map +0 -1
- package/types/source/configuration.d.ts.map +0 -1
- package/types/source/core.d.ts +0 -2
- package/types/source/core.d.ts.map +0 -1
- package/types/source/express-router.d.ts.map +0 -1
- package/types/source/koa-router.d.ts.map +0 -1
- package/types/source/mod.d.ts.map +0 -1
- package/types/source/node-router.d.ts.map +0 -1
- package/types/source/polyfill.d.ts +0 -2
- package/types/source/polyfill.d.ts.map +0 -1
- package/types/source/postgres.d.ts.map +0 -1
package/core/configuration.js
CHANGED
|
@@ -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 {
|
|
13
|
+
* @property {T} fallback
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
13
|
-
* @property {
|
|
14
|
-
* @property {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
46
|
-
* @param {T}
|
|
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(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
struct[Configuration.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
|
-
* @
|
|
59
|
-
* @returns {
|
|
142
|
+
* @param {SpecOptions<string>} options
|
|
143
|
+
* @returns {Structure<string>}
|
|
60
144
|
*/
|
|
61
|
-
string(
|
|
62
|
-
if (typeof
|
|
63
|
-
throw new TypeError(
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
* @
|
|
75
|
-
* @returns {
|
|
158
|
+
* @param {SpecOptions<number>} options
|
|
159
|
+
* @returns {Structure<number>}
|
|
76
160
|
*/
|
|
77
|
-
|
|
78
|
-
if (typeof
|
|
79
|
-
throw new TypeError("
|
|
161
|
+
number(options) {
|
|
162
|
+
if (typeof options.fallback !== "number") {
|
|
163
|
+
throw new TypeError("options.fallback must be a number");
|
|
80
164
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 =
|
|
100
|
-
? this.options.getEnvironmentVariable(
|
|
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
|
-
|
|
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 {
|
|
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
|
|
264
|
+
return spec.process({});
|
|
118
265
|
}
|
|
119
266
|
|
|
120
267
|
// Fail outside the try-catch to surface structure errors
|
|
121
|
-
|
|
122
|
-
await this.options.parse(file)
|
|
123
|
-
|
|
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
|
-
/** @
|
|
129
|
-
getUsage(
|
|
130
|
-
const { fallback, fields } = this.
|
|
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
|
-
* @
|
|
301
|
+
* @param {unknown} struct
|
|
151
302
|
* @param {string} [prefix]
|
|
152
303
|
* @returns {{ config: any, fields: [string, string] }}
|
|
153
304
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 {};
|