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 +27 -0
- package/README.md +67 -13
- package/core/configuration.d.ts +115 -17
- package/core/configuration.js +208 -69
- package/core/configuration.test.js +216 -17
- package/core/http.d.ts +30 -11
- package/core/http.js +42 -17
- package/core/http.test.js +57 -35
- package/core/mod.d.ts +1 -0
- package/core/mod.js +1 -0
- package/core/structures.d.ts +32 -22
- package/core/structures.js +126 -29
- package/core/structures.test.js +243 -30
- package/package.json +1 -1
- package/source/configuration.d.ts +2 -2
- package/source/configuration.js +2 -2
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
|
|
244
|
+
export function getConfigStruct() {
|
|
245
245
|
return config.object({
|
|
246
|
-
env: config.string({
|
|
247
|
-
|
|
248
|
-
|
|
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,
|
|
277
|
+
return config.load(path, getConfigStruct());
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
// TypeScript thought:
|
|
278
|
-
// export type Configuration = Infer<ReturnType<typeof
|
|
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(
|
|
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(
|
|
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,
|
|
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`.
|
package/core/configuration.d.ts
CHANGED
|
@@ -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}
|
|
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>>>(
|
|
78
|
+
object<T extends Record<string, Structure<any>>>(options: T): Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]>; }>;
|
|
12
79
|
/**
|
|
13
|
-
* @
|
|
80
|
+
* @param {SpecOptions<string>} options
|
|
14
81
|
* @returns {Structure<string>}
|
|
15
82
|
*/
|
|
16
|
-
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
|
-
* @
|
|
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<
|
|
22
|
-
/**
|
|
23
|
-
|
|
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<
|
|
31
|
-
/** @
|
|
32
|
-
getUsage
|
|
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
|
-
* @
|
|
119
|
+
* @param {unknown} struct
|
|
35
120
|
* @param {string} [prefix]
|
|
36
121
|
* @returns {{ config: any, fields: [string, string] }}
|
|
37
122
|
*/
|
|
38
|
-
|
|
123
|
+
describe(value: any, prefix?: string): {
|
|
39
124
|
config: any;
|
|
40
125
|
fields: [string, string];
|
|
41
126
|
};
|
|
42
|
-
/** @param {Structure<any>}
|
|
43
|
-
getJSONSchema(
|
|
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:
|
|
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>;
|
package/core/configuration.js
CHANGED
|
@@ -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 {
|
|
13
|
+
* @property {T} fallback
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
* @
|
|
17
|
-
* @
|
|
18
|
-
* @property {
|
|
19
|
-
* @property {
|
|
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}
|
|
140
|
+
* @param {T} options
|
|
48
141
|
* @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
|
|
49
142
|
*/
|
|
50
|
-
object(
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
* @
|
|
153
|
+
* @param {SpecOptions<string>} options
|
|
58
154
|
* @returns {Structure<string>}
|
|
59
155
|
*/
|
|
60
|
-
string(
|
|
61
|
-
if (typeof
|
|
62
|
-
throw new TypeError(
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
* @
|
|
199
|
+
* @param {SpecOptions<string|URL>} options
|
|
71
200
|
* @returns {Structure<URL>}
|
|
72
201
|
*/
|
|
73
|
-
url(
|
|
74
|
-
if (
|
|
75
|
-
|
|
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(
|
|
78
|
-
struct[Configuration.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
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 =
|
|
89
|
-
? this.options.getEnvironmentVariable(
|
|
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
|
-
|
|
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
|
-
/** @
|
|
122
|
-
getUsage(
|
|
123
|
-
const { fallback, fields } = this.
|
|
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
|
-
* @
|
|
312
|
+
* @param {unknown} struct
|
|
144
313
|
* @param {string} [prefix]
|
|
145
314
|
* @returns {{ config: any, fields: [string, string] }}
|
|
146
315
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
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>}
|
|
182
|
-
getJSONSchema(
|
|
183
|
-
return
|
|
320
|
+
/** * @param {Structure<any>} struct */
|
|
321
|
+
getJSONSchema(struct) {
|
|
322
|
+
return struct.getSchema();
|
|
184
323
|
}
|
|
185
324
|
}
|