gruber 0.1.0 → 0.3.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 +31 -0
- package/README.md +214 -10
- package/core/configuration.d.ts +59 -0
- package/core/configuration.js +39 -36
- package/core/configuration.test.d.ts +1 -0
- package/core/configuration.test.js +32 -42
- package/core/fetch-router.d.ts +51 -0
- package/core/fetch-router.js +36 -23
- package/core/fetch-router.test.d.ts +1 -0
- package/core/fetch-router.test.js +50 -22
- package/core/http.d.ts +54 -0
- package/core/http.js +20 -21
- package/core/http.test.d.ts +1 -0
- package/core/migrator.d.ts +55 -0
- package/core/migrator.test.d.ts +1 -0
- package/core/migrator.test.js +3 -4
- package/core/mod.d.ts +6 -0
- package/core/postgres.d.ts +42 -0
- package/core/structures.d.ts +78 -0
- package/core/structures.js +202 -0
- package/core/structures.test.d.ts +1 -0
- package/core/structures.test.js +349 -0
- package/core/test-deps.d.ts +1 -0
- package/core/test-deps.js +1 -1
- package/core/types.d.ts +22 -0
- package/core/types.ts +41 -0
- package/core/utilities.d.ts +12 -0
- package/core/utilities.js +10 -4
- package/core/utilities.test.d.ts +1 -0
- package/package.json +12 -2
- package/source/configuration.d.ts +19 -0
- package/source/configuration.js +0 -2
- package/source/core.d.ts +1 -0
- package/source/express-router.d.ts +27 -0
- package/source/express-router.js +1 -1
- package/source/koa-router.d.ts +32 -0
- package/source/mod.d.ts +4 -0
- package/source/mod.js +2 -2
- package/source/node-router.d.ts +42 -0
- package/source/node-router.js +9 -2
- package/source/package-lock.json +3 -9
- package/source/package.json +8 -1
- package/source/polyfill.d.ts +1 -0
- package/source/postgres.d.ts +31 -0
- package/source/postgres.js +18 -1
- package/tsconfig.json +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
This file documents notable changes to the project
|
|
4
4
|
|
|
5
|
+
## 0.3.0
|
|
6
|
+
|
|
7
|
+
**new**
|
|
8
|
+
|
|
9
|
+
- Removed use of superstruct in favour of new `structures.js` implementation
|
|
10
|
+
- Added `getJSONSchema` method to `Configuration`
|
|
11
|
+
|
|
12
|
+
**fixed**
|
|
13
|
+
|
|
14
|
+
- Node.js types should work now
|
|
15
|
+
- Node.js types includes a url-pattern polyfil
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
**new**
|
|
20
|
+
|
|
21
|
+
- Added and documented `FetchRouter`
|
|
22
|
+
- Generate typings for Node.js
|
|
23
|
+
- Unhandled route errors are logged to the console
|
|
24
|
+
|
|
25
|
+
**fixes**
|
|
26
|
+
|
|
27
|
+
- Configuration is better typed to be able to infer the final structure
|
|
28
|
+
- Add missing NPM package information
|
|
29
|
+
|
|
30
|
+
**docs**
|
|
31
|
+
|
|
32
|
+
- Add a section on installation
|
|
33
|
+
- Add a section on the releaes process
|
|
34
|
+
- Add information about `loader` and `formatMarkdownTable`
|
|
35
|
+
|
|
5
36
|
## 0.1.0
|
|
6
37
|
|
|
7
38
|
🎉 Everything is new!
|
package/README.md
CHANGED
|
@@ -77,6 +77,27 @@ A Gruber app should be run behind a reverse proxy and that can do those things f
|
|
|
77
77
|
- Minimal — start small, carefully add features and consider removing them
|
|
78
78
|
- No magic — it's confusing when you don't know whats going on
|
|
79
79
|
|
|
80
|
+
## Install
|
|
81
|
+
|
|
82
|
+
**Node.js**
|
|
83
|
+
|
|
84
|
+
Gruber is available on NPM as [gruber](https://www.npmjs.com/package/gruber).
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# cd to/your/project
|
|
88
|
+
npm install gruber
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Deno**
|
|
92
|
+
|
|
93
|
+
> WORK IN PROGRESS
|
|
94
|
+
|
|
95
|
+
Gruber is available at [esm.r0b.io/gruber@0.1.0/mod.ts](https://esm.r0b.io/gruber@0.1.0/mod.ts).
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { defineRoute } from "https://esm.r0b.io/gruber@0.1.0/mod.ts";
|
|
99
|
+
```
|
|
100
|
+
|
|
80
101
|
## HTTP server
|
|
81
102
|
|
|
82
103
|
First a HTTP route to do something:
|
|
@@ -214,11 +235,11 @@ Building on the [HTTP server](#http-server) above, we'll setup configuration. St
|
|
|
214
235
|
**config.js**
|
|
215
236
|
|
|
216
237
|
```js
|
|
217
|
-
import
|
|
238
|
+
import fs from "node:fs";
|
|
218
239
|
import { getNodeConfiguration } from "gruber";
|
|
219
240
|
|
|
220
241
|
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
|
|
221
|
-
const config = getNodeConfiguration(
|
|
242
|
+
const config = getNodeConfiguration();
|
|
222
243
|
|
|
223
244
|
export function getSpecification() {
|
|
224
245
|
return config.object({
|
|
@@ -265,6 +286,11 @@ export const appConfig = await loadConfiguration(
|
|
|
265
286
|
export function getConfigurationUsage() {
|
|
266
287
|
return config.getUsage(getSpecification());
|
|
267
288
|
}
|
|
289
|
+
|
|
290
|
+
// Export a method to generate a JSON Schema for the configuration
|
|
291
|
+
export function getConfigurationSchema() {
|
|
292
|
+
return config.getJSONSchema(getSpecification());
|
|
293
|
+
}
|
|
268
294
|
```
|
|
269
295
|
|
|
270
296
|
### Usage info
|
|
@@ -306,11 +332,11 @@ You can provide a configuration file like **config.json** to load through the co
|
|
|
306
332
|
"selfUrl": "http://localhost:3000",
|
|
307
333
|
"meta": {
|
|
308
334
|
"name": "gruber-app",
|
|
309
|
-
"version": "1.2.3"
|
|
335
|
+
"version": "1.2.3"
|
|
310
336
|
},
|
|
311
337
|
"database": {
|
|
312
|
-
"url": "postgres://user:secret@localhost:5432/database"
|
|
313
|
-
}
|
|
338
|
+
"url": "postgres://user:secret@localhost:5432/database"
|
|
339
|
+
}
|
|
314
340
|
}
|
|
315
341
|
```
|
|
316
342
|
|
|
@@ -651,7 +677,6 @@ To see how it works, look at the [Node](./node/source/configuration.js) and [Den
|
|
|
651
677
|
You can use the static `getOptions` method both subclasses provide and override the parts you want.
|
|
652
678
|
These are the options:
|
|
653
679
|
|
|
654
|
-
- `superstruct` — Configuration is based on [superstruct](https://docs.superstructjs.org/), you can pass your own instance if you like.
|
|
655
680
|
- `readTextFile(url)` — How to load a text file from the file system
|
|
656
681
|
- `getEnvironmentVariable(key)` — Return a matching environment "variable" for a key
|
|
657
682
|
- `getCommandArgument(key)` — Get the corresponding "flag" from a CLI argument
|
|
@@ -663,10 +688,9 @@ For example, to override in Node:
|
|
|
663
688
|
```js
|
|
664
689
|
import { Configuration, getNodeConfigOptions } from "gruber";
|
|
665
690
|
import Yaml from "yaml";
|
|
666
|
-
import superstruct from "superstruct";
|
|
667
691
|
|
|
668
692
|
const config = new Configuration({
|
|
669
|
-
...getNodeConfigOptions(
|
|
693
|
+
...getNodeConfigOptions(),
|
|
670
694
|
getEnvionmentVariable: () => undefined,
|
|
671
695
|
stringify: (v) => Yaml.stringify(v),
|
|
672
696
|
parse: (v) => Yaml.parse(v),
|
|
@@ -797,6 +821,49 @@ teapot.toResponse();
|
|
|
797
821
|
Currently, you can't set the body of the generated Response objects.
|
|
798
822
|
This would be nice to have in the future, but the API should be thoughtfully designed first.
|
|
799
823
|
|
|
824
|
+
### FetchRouter
|
|
825
|
+
|
|
826
|
+
`FetchRouter` is a web-native router for routes defined with `defineRoute`.
|
|
827
|
+
|
|
828
|
+
```js
|
|
829
|
+
import { FetchRouter, defineRoute } from "gruber";
|
|
830
|
+
|
|
831
|
+
const routes = [defineRoute("..."), defineRoute("..."), defineRoute("...")];
|
|
832
|
+
|
|
833
|
+
const router = new FetchRouter({
|
|
834
|
+
routes,
|
|
835
|
+
errorHandler(error, request) {
|
|
836
|
+
console.log("Route error", error);
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
All options to the `FetchRouter` constructor are optional and you can create a router without any options if you want.
|
|
842
|
+
|
|
843
|
+
`routes` are the route definitions you want the router to processes, the router will handle a request based on the first route that matches.
|
|
844
|
+
So order is important.
|
|
845
|
+
|
|
846
|
+
`errorHandler` is called if a non-`HTTPError` or a 5xx `HTTPError` is thrown.
|
|
847
|
+
It is called with the offending error and the request it is associated with.
|
|
848
|
+
|
|
849
|
+
> NOTE: The `errorHandler` could do more in the future, like create it's own Response or mutate the existing response.
|
|
850
|
+
> This has not been design and is left open to future development if it becomes important.
|
|
851
|
+
|
|
852
|
+
**getResponse**
|
|
853
|
+
|
|
854
|
+
`getResponse` is the main method on a router.
|
|
855
|
+
Use it to get a `Response` from the provided request, based on the router's route definitions.
|
|
856
|
+
|
|
857
|
+
```js
|
|
858
|
+
const response = await router.getResponse(new Request("http://localhost"));
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
There are some unstable internal methods too:
|
|
862
|
+
|
|
863
|
+
- `findMatchingRoutes(request)` is a generator function to get the first route definition that matches the supplied request. It's a generator so as few routes are matched as possible and execution can be stopped if you like.
|
|
864
|
+
- `processMatches(request, matches)` attempts to get a `Response` from a request and an Iterator of route definitions.
|
|
865
|
+
- `handleError(error, request)` converts a error into a Response and triggers the `errorHandler`
|
|
866
|
+
|
|
800
867
|
### Postgres
|
|
801
868
|
|
|
802
869
|
#### getPostgresMigratorOptions
|
|
@@ -804,6 +871,135 @@ This would be nice to have in the future, but the API should be thoughtfully des
|
|
|
804
871
|
`getPostgresMigratorOptions` generates the default options for a `PostgresMigrator`.
|
|
805
872
|
You can use it and override parts of it to customise how the postgres migrator works.
|
|
806
873
|
|
|
874
|
+
### Utilities
|
|
875
|
+
|
|
876
|
+
#### loader
|
|
877
|
+
|
|
878
|
+
`loader` let's you memoize the result of a function to create a singleton from it.
|
|
879
|
+
It works synchronously or with promises.
|
|
880
|
+
|
|
881
|
+
```js
|
|
882
|
+
import { loader } from "gruber";
|
|
883
|
+
|
|
884
|
+
const useRedis = loader(async () => {
|
|
885
|
+
return "connect to the database somehow...";
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// Then elsewhere
|
|
889
|
+
const redis = await useRedis();
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
#### formatMarkdownTable
|
|
893
|
+
|
|
894
|
+
`formatMarkdownTable` generates a pretty markdown table based on an array of rows and the desired column names.
|
|
895
|
+
|
|
896
|
+
```js
|
|
897
|
+
import { formatMarkdownTable } from "gruber";
|
|
898
|
+
|
|
899
|
+
const table = formatMarkdownTable(
|
|
900
|
+
[
|
|
901
|
+
{ name: "Geoff Testington", age: 42 },
|
|
902
|
+
{ name: "Jess Smith", age: 32 },
|
|
903
|
+
{ name: "Tyler Rockwell" },
|
|
904
|
+
],
|
|
905
|
+
["name", "age"],
|
|
906
|
+
"~",
|
|
907
|
+
);
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
This will generate the table:
|
|
911
|
+
|
|
912
|
+
```
|
|
913
|
+
| name | age |
|
|
914
|
+
| ================ | === |
|
|
915
|
+
| Geoff Testington | 42 |
|
|
916
|
+
| Jess Smith | 32 |
|
|
917
|
+
| Tyler Rockwell | ~ |
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
#### Structure
|
|
921
|
+
|
|
922
|
+
This is an internal primative for validating objects, strings, numbers and URLs for use in [Configuration](#configuration).
|
|
923
|
+
It is based on a very specific use of [superstruct](https://github.com/ianstormtaylor/superstruct) which it made sense to internalise to make the code base more portable.
|
|
924
|
+
A `Structure` is a type that validates a value is correct by throwing an error if validation fails, i.e. the wrong type is passed.
|
|
925
|
+
Every struct has an intrinsic `fallback` so that if no value (`undefined`) is passed, that is used instead.
|
|
926
|
+
|
|
927
|
+
```js
|
|
928
|
+
import { Structure } from "gruber/structures.js";
|
|
929
|
+
|
|
930
|
+
// A string primative, or use "Geoff Testington" if no value is passed.
|
|
931
|
+
const name = Structure.string("Geoff Testington");
|
|
932
|
+
|
|
933
|
+
// A URL instance or a string that contains a valid URL, always converting to a URL
|
|
934
|
+
const website = Structure.url("https://example.com");
|
|
935
|
+
|
|
936
|
+
// A number primative, falling back to 42
|
|
937
|
+
const age = Structure.number(42);
|
|
938
|
+
|
|
939
|
+
// An object with all of the fields above and nothing else
|
|
940
|
+
// defaulting to create { name: "Geoff..", age: 42, website: "https..." } with the same fallback values
|
|
941
|
+
const person = Structure.object({ name, age, website });
|
|
942
|
+
|
|
943
|
+
// Process the Structure and get a value out. The returned value is strongly typed!
|
|
944
|
+
// This will throw if the value passed does not match the schema.
|
|
945
|
+
const value = person.process(/* ... */);
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
Those static Structure methods return a `Structure` instance. You can also create your own types with the constructor. This example shows how to do that, and also starts to unveil how the internals work a bit with [StructError](#structerror).
|
|
949
|
+
|
|
950
|
+
```js
|
|
951
|
+
import { Structure, StructError } from "gruber/structures.js";
|
|
952
|
+
|
|
953
|
+
// Create a new boolean structure (this should probably be included as Structure.boolean tbh)
|
|
954
|
+
const boolean = new Structure(
|
|
955
|
+
{ type: "boolean", default: false },
|
|
956
|
+
(input, context) => {
|
|
957
|
+
if (input === undefined) return false;
|
|
958
|
+
if (typeof input !== "boolean") {
|
|
959
|
+
throw new StructError("Expected a boolean", context?.path);
|
|
960
|
+
}
|
|
961
|
+
return input;
|
|
962
|
+
},
|
|
963
|
+
);
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
To create a custom Structure, you give it a [JSON schema](https://json-schema.org/) and a "process" function.
|
|
967
|
+
The function is called to validate a value against the structure. It should return the processed value or throw a `StructError`.
|
|
968
|
+
|
|
969
|
+
The `context` object might not be set and this means the struct is at the root level. If it is nested in an `object` then the context contains the path that the struct is located at, all the way from the root object. That path is expressed as an array of strings. That path is used to generate friendlier error messages to explain which nested field failed.
|
|
970
|
+
|
|
971
|
+
With a Structure, you can generate a JSON Schema:
|
|
972
|
+
|
|
973
|
+
```js
|
|
974
|
+
import { Structure } from "gruber/structures.js";
|
|
975
|
+
|
|
976
|
+
const person = Structure.object({
|
|
977
|
+
name: Structure.string("Geoff Testington"),
|
|
978
|
+
age: Structure.number(42),
|
|
979
|
+
website: Structure.url("https://example.com"),
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
console.log(JSON.stringify(person.getSchema(), null, 2));
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
This is a bit WIP, but you could use this to generate a JSON schema to lint configurations in your IDE.
|
|
986
|
+
|
|
987
|
+
#### StructError
|
|
988
|
+
|
|
989
|
+
This Error subclass contains extra information about why parsing a `Structure` failed.
|
|
990
|
+
|
|
991
|
+
- The `message` field is a description of what went wrong, in the context of the structure.
|
|
992
|
+
- An extra `path` field exists to describe the path from the root object to get to this failed structure
|
|
993
|
+
- `children` is also available to let a structure have multiple child errors, i.e. for an object to have failures for each of the fields that have failed.
|
|
994
|
+
|
|
995
|
+
On the error, there are also methods to help use it:
|
|
996
|
+
|
|
997
|
+
- `toFriendlyString` goes through all nested failures and outputs a single message to describe everything that went wrong.
|
|
998
|
+
- `getOneLiner` converts the error to a succint one-line error message, concatentating the path and message
|
|
999
|
+
- `[Symbol.iterator]` is also available if you want to loop through all children nodes, only those that do not have children themselves.
|
|
1000
|
+
|
|
1001
|
+
There is also the static method `StructError.chain(error, context)` which is useful for catching errors and applying a context to them (if they are not already a StructError).
|
|
1002
|
+
|
|
807
1003
|
## Node.js library
|
|
808
1004
|
|
|
809
1005
|
There are some specific helpers to help use Gruber in Node.js apps.
|
|
@@ -859,7 +1055,7 @@ For older version of Node.js that don't support the latest web-standards,
|
|
|
859
1055
|
there is a polyfil import you can use to add support for them to your runtime.
|
|
860
1056
|
|
|
861
1057
|
```js
|
|
862
|
-
import "gruber/
|
|
1058
|
+
import "gruber/polyfill.js";
|
|
863
1059
|
```
|
|
864
1060
|
|
|
865
1061
|
This currently polyfils these APIs:
|
|
@@ -913,6 +1109,15 @@ const server = http.createServer((req) => {
|
|
|
913
1109
|
});
|
|
914
1110
|
```
|
|
915
1111
|
|
|
1112
|
+
## Release process
|
|
1113
|
+
|
|
1114
|
+
1. Generate a new version at the root with `npm version <version>`
|
|
1115
|
+
2. Run the bundle `./bundle.js`
|
|
1116
|
+
3. Publish the node module
|
|
1117
|
+
1. `cd bundle/node`
|
|
1118
|
+
2. `npm publish`
|
|
1119
|
+
4. Copy the deno source to the S3 bucket — `bundle/deno` → `esm.r0b.io/gruber@VERSION/`
|
|
1120
|
+
|
|
916
1121
|
---
|
|
917
1122
|
|
|
918
1123
|
<!-- -->
|
|
@@ -992,7 +1197,6 @@ retryWithBackoff({
|
|
|
992
1197
|
|
|
993
1198
|
## Rob's notes
|
|
994
1199
|
|
|
995
|
-
- should exposing `appConfig` be a best practice?
|
|
996
1200
|
- `core` tests are deno because it's hard to do both and Deno is more web-standards based
|
|
997
1201
|
- json schema for configuration specs?
|
|
998
1202
|
- note or info about loading dot-env files
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class Configuration {
|
|
2
|
+
static spec: symbol;
|
|
3
|
+
/** @param {ConfigurationOptions} options */
|
|
4
|
+
constructor(options: ConfigurationOptions);
|
|
5
|
+
/** @type {ConfigurationOptions} */ options: ConfigurationOptions;
|
|
6
|
+
/**
|
|
7
|
+
* @template {Record<string, Structure<any>>} T
|
|
8
|
+
* @param {T} spec
|
|
9
|
+
* @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
|
|
10
|
+
*/
|
|
11
|
+
object<T extends Record<string, Structure<any>>>(spec: T): Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]>; }>;
|
|
12
|
+
/**
|
|
13
|
+
* @template {SpecOptions} Spec @param {Spec} spec
|
|
14
|
+
* @returns {Structure<string>}
|
|
15
|
+
*/
|
|
16
|
+
string<Spec extends SpecOptions>(spec?: Spec): Structure<string>;
|
|
17
|
+
/**
|
|
18
|
+
* @template {SpecOptions} Spec @param {Spec} spec
|
|
19
|
+
* @returns {Structure<URL>}
|
|
20
|
+
*/
|
|
21
|
+
url<Spec_1 extends SpecOptions>(spec: Spec_1): Structure<URL>;
|
|
22
|
+
/** @param {SpecOptions} spec */
|
|
23
|
+
_getValue(spec: SpecOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* @template T
|
|
26
|
+
* @param {URL} url
|
|
27
|
+
* @param {Structure<T>} spec
|
|
28
|
+
* @returns {Promise<T>}
|
|
29
|
+
*/
|
|
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;
|
|
33
|
+
/**
|
|
34
|
+
* @template T @param {T} config
|
|
35
|
+
* @param {string} [prefix]
|
|
36
|
+
* @returns {{ config: any, fields: [string, string] }}
|
|
37
|
+
*/
|
|
38
|
+
describeSpecification<T_3>(spec: any, prefix?: string): {
|
|
39
|
+
config: any;
|
|
40
|
+
fields: [string, string];
|
|
41
|
+
};
|
|
42
|
+
/** @param {Structure<any>} spec */
|
|
43
|
+
getJSONSchema(spec: Structure<any>): {
|
|
44
|
+
$schema: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export type SpecOptions = {
|
|
48
|
+
variable?: string;
|
|
49
|
+
flag?: string;
|
|
50
|
+
fallback: string;
|
|
51
|
+
};
|
|
52
|
+
export type ConfigurationOptions = {
|
|
53
|
+
readTextFile: (url: URL) => Promise<string | null>;
|
|
54
|
+
getEnvironmentVariable: (key: string) => (string | undefined);
|
|
55
|
+
getCommandArgument: (key: string) => (string | undefined);
|
|
56
|
+
stringify: (value: any) => (string | Promise<string>);
|
|
57
|
+
parse: (value: string) => (any);
|
|
58
|
+
};
|
|
59
|
+
import { Structure } from "./structures.js";
|
package/core/configuration.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
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
|
/**
|
|
4
9
|
* @typedef {object} SpecOptions
|
|
@@ -9,7 +14,6 @@ import { formatMarkdownTable } from "./utilities.js";
|
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* @typedef {object} ConfigurationOptions
|
|
12
|
-
* @property {import("superstruct")} superstruct
|
|
13
17
|
* @property {(url: URL) => Promise<string | null>} readTextFile
|
|
14
18
|
* @property {(key: string) => (string | undefined)} getEnvironmentVariable
|
|
15
19
|
* @property {(key: string) => (string | undefined)} getCommandArgument
|
|
@@ -18,7 +22,6 @@ import { formatMarkdownTable } from "./utilities.js";
|
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
const _requiredOptions = [
|
|
21
|
-
"superstruct",
|
|
22
25
|
"readTextFile",
|
|
23
26
|
"getEnvironmentVariable",
|
|
24
27
|
"getCommandArgument",
|
|
@@ -29,7 +32,7 @@ const _requiredOptions = [
|
|
|
29
32
|
export class Configuration {
|
|
30
33
|
static spec = Symbol("Configuration.spec");
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
/** @type {ConfigurationOptions} */ options;
|
|
33
36
|
|
|
34
37
|
/** @param {ConfigurationOptions} options */
|
|
35
38
|
constructor(options) {
|
|
@@ -39,51 +42,41 @@ export class Configuration {
|
|
|
39
42
|
this.options = options;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* @template {Record<string, Structure<any>>} T
|
|
47
|
+
* @param {T} spec
|
|
48
|
+
* @returns {Structure<{ [K in keyof T]: import("./structures.js").Infer<T[K]> }>}
|
|
49
|
+
*/
|
|
43
50
|
object(spec) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{},
|
|
48
|
-
),
|
|
49
|
-
{ [Configuration.spec]: { type: "object", value: spec } },
|
|
50
|
-
);
|
|
51
|
+
const struct = Structure.object(spec);
|
|
52
|
+
struct[Configuration.spec] = { type: "object", value: spec };
|
|
53
|
+
return struct;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* @template {SpecOptions} Spec @param {Spec} spec
|
|
58
|
+
* @returns {Structure<string>}
|
|
55
59
|
*/
|
|
56
60
|
string(spec = {}) {
|
|
57
61
|
if (typeof spec.fallback !== "string") {
|
|
58
62
|
throw new TypeError("spec.fallback must be a string: " + spec.fallback);
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this._getValue(spec),
|
|
64
|
-
),
|
|
65
|
-
{ [Configuration.spec]: { type: "string", value: spec } },
|
|
66
|
-
);
|
|
64
|
+
const struct = Structure.string(this._getValue(spec));
|
|
65
|
+
struct[Configuration.spec] = { type: "string", value: spec };
|
|
66
|
+
return struct;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* @template {SpecOptions} Spec @param {Spec} spec
|
|
71
|
+
* @returns {Structure<URL>}
|
|
71
72
|
*/
|
|
72
73
|
url(spec) {
|
|
73
74
|
if (typeof spec.fallback !== "string") {
|
|
74
75
|
throw new TypeError("spec.fallback must be a string");
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.options.superstruct.instance(URL),
|
|
80
|
-
this.options.superstruct.string(),
|
|
81
|
-
(value) => new URL(value),
|
|
82
|
-
),
|
|
83
|
-
this._getValue(spec),
|
|
84
|
-
),
|
|
85
|
-
{ [Configuration.spec]: { type: "url", value: spec } },
|
|
86
|
-
);
|
|
77
|
+
const struct = Structure.url(this._getValue(spec));
|
|
78
|
+
struct[Configuration.spec] = { type: "url", value: spec };
|
|
79
|
+
return struct;
|
|
87
80
|
}
|
|
88
81
|
|
|
89
82
|
/** @param {SpecOptions} spec */
|
|
@@ -102,22 +95,27 @@ export class Configuration {
|
|
|
102
95
|
/**
|
|
103
96
|
* @template T
|
|
104
97
|
* @param {URL} url
|
|
105
|
-
* @param {
|
|
98
|
+
* @param {Structure<T>} spec
|
|
99
|
+
* @returns {Promise<T>}
|
|
106
100
|
*/
|
|
107
101
|
async load(url, spec) {
|
|
108
102
|
const file = await this.options.readTextFile(url);
|
|
109
103
|
|
|
110
104
|
// Catch missing files and create a default configuration
|
|
111
105
|
if (!file) {
|
|
112
|
-
return
|
|
106
|
+
return spec.process({});
|
|
113
107
|
}
|
|
114
108
|
|
|
115
109
|
// Fail outside the try-catch to surface structure errors
|
|
116
|
-
|
|
117
|
-
await this.options.parse(file)
|
|
118
|
-
|
|
119
|
-
"Configuration failed to parse"
|
|
120
|
-
|
|
110
|
+
try {
|
|
111
|
+
return spec.process(await this.options.parse(file));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Configuration failed to parse");
|
|
114
|
+
if (error instanceof StructError) {
|
|
115
|
+
error.message = error.toFriendlyString();
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
/** @template T @param {T} config */
|
|
@@ -179,4 +177,9 @@ export class Configuration {
|
|
|
179
177
|
}
|
|
180
178
|
throw new TypeError("Invalid [Configuration.spec].type '" + type + "'");
|
|
181
179
|
}
|
|
180
|
+
|
|
181
|
+
/** @param {Structure<any>} spec */
|
|
182
|
+
getJSONSchema(spec) {
|
|
183
|
+
return spec.getSchema();
|
|
184
|
+
}
|
|
182
185
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|