@kensio/simnaril 0.1.0 → 0.1.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.
Files changed (82) hide show
  1. package/README.md +2 -0
  2. package/dist/api.d.ts +25 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +35 -31
  5. package/dist/api.js.map +1 -1
  6. package/dist/http/decode-form.d.ts +38 -0
  7. package/dist/http/decode-form.d.ts.map +1 -0
  8. package/dist/http/decode-form.js +119 -0
  9. package/dist/http/decode-form.js.map +1 -0
  10. package/dist/http/error-formatter.d.ts +13 -0
  11. package/dist/http/error-formatter.d.ts.map +1 -0
  12. package/dist/http/error-formatter.js +2 -0
  13. package/dist/http/error-formatter.js.map +1 -0
  14. package/dist/http/operation-router.d.ts +2 -0
  15. package/dist/http/operation-router.d.ts.map +1 -1
  16. package/dist/http/operation-router.js +16 -1
  17. package/dist/http/operation-router.js.map +1 -1
  18. package/dist/http/operation.d.ts +2 -1
  19. package/dist/http/operation.d.ts.map +1 -1
  20. package/dist/http/operation.js.map +1 -1
  21. package/dist/http/path-parameter.d.ts +3 -0
  22. package/dist/http/path-parameter.d.ts.map +1 -0
  23. package/dist/http/path-parameter.js +12 -0
  24. package/dist/http/path-parameter.js.map +1 -0
  25. package/dist/http/raw-operation.d.ts +16 -0
  26. package/dist/http/raw-operation.d.ts.map +1 -0
  27. package/dist/http/raw-operation.js +27 -0
  28. package/dist/http/raw-operation.js.map +1 -0
  29. package/dist/http/request-decoder.d.ts +30 -0
  30. package/dist/http/request-decoder.d.ts.map +1 -0
  31. package/dist/http/request-decoder.js +18 -0
  32. package/dist/http/request-decoder.js.map +1 -0
  33. package/dist/http/resource-operation-registry.d.ts +1 -1
  34. package/dist/http/resource-operation-registry.d.ts.map +1 -1
  35. package/dist/http/resource-operation-registry.js +6 -3
  36. package/dist/http/resource-operation-registry.js.map +1 -1
  37. package/dist/http/resource-operation.d.ts +2 -1
  38. package/dist/http/resource-operation.d.ts.map +1 -1
  39. package/dist/http/resource-operation.js +3 -3
  40. package/dist/http/resource-operation.js.map +1 -1
  41. package/dist/http/resource-path.d.ts +13 -0
  42. package/dist/http/resource-path.d.ts.map +1 -0
  43. package/dist/http/resource-path.js +23 -0
  44. package/dist/http/resource-path.js.map +1 -0
  45. package/dist/http/rest-resource-operation-defaults.d.ts +10 -0
  46. package/dist/http/rest-resource-operation-defaults.d.ts.map +1 -0
  47. package/dist/http/rest-resource-operation-defaults.js +8 -0
  48. package/dist/http/rest-resource-operation-defaults.js.map +1 -0
  49. package/dist/http/rest-resource-operations.d.ts +3 -3
  50. package/dist/http/rest-resource-operations.d.ts.map +1 -1
  51. package/dist/http/rest-resource-operations.js +37 -51
  52. package/dist/http/rest-resource-operations.js.map +1 -1
  53. package/dist/http/semantic-http-operation.d.ts +2 -1
  54. package/dist/http/semantic-http-operation.d.ts.map +1 -1
  55. package/dist/http/semantic-http-operation.js.map +1 -1
  56. package/dist/http/supplied-rest-resource-operation.d.ts +22 -0
  57. package/dist/http/supplied-rest-resource-operation.d.ts.map +1 -0
  58. package/dist/http/supplied-rest-resource-operation.js +27 -0
  59. package/dist/http/supplied-rest-resource-operation.js.map +1 -0
  60. package/dist/index.d.ts +10 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +6 -3
  63. package/dist/index.js.map +1 -1
  64. package/dist/resource.d.ts +1 -1
  65. package/dist/resource.d.ts.map +1 -1
  66. package/dist/resource.js +1 -1
  67. package/dist/resource.js.map +1 -1
  68. package/dist/rest-resource-operation.d.ts +20 -2
  69. package/dist/rest-resource-operation.d.ts.map +1 -1
  70. package/dist/rest-resource.d.ts +2 -0
  71. package/dist/rest-resource.d.ts.map +1 -1
  72. package/dist/rest-resource.js +9 -1
  73. package/dist/rest-resource.js.map +1 -1
  74. package/dist/unimplemented-route-error.d.ts +2 -1
  75. package/dist/unimplemented-route-error.d.ts.map +1 -1
  76. package/dist/unimplemented-route-error.js +4 -2
  77. package/dist/unimplemented-route-error.js.map +1 -1
  78. package/dist/webhooks.d.ts +98 -0
  79. package/dist/webhooks.d.ts.map +1 -0
  80. package/dist/webhooks.js +129 -0
  81. package/dist/webhooks.js.map +1 -0
  82. package/package.json +1 -1
package/README.md CHANGED
@@ -32,6 +32,8 @@ Simnaril requires Node.js 24 or later.
32
32
  - [Simulation environments](docs/simulation-environments/README.md)
33
33
  - [Resource state](docs/resource-state/README.md)
34
34
  - [REST resources](docs/rest-resources/README.md)
35
+ - [Request bodies](docs/request-bodies/README.md)
35
36
  - [Custom operations](docs/custom-operations/README.md)
36
37
  - [Middleware](docs/middleware/README.md)
38
+ - [Webhooks](docs/webhooks/README.md)
37
39
  - [Composing a simulation](docs/composing-a-simulation/README.md)
package/dist/api.d.ts CHANGED
@@ -1,15 +1,37 @@
1
- import { type HttpMiddleware, type HttpOperationContext, type RawHttpOperation } from "./http/operation.js";
1
+ import type { HttpMiddleware, RawHttpOperation } from "./http/operation.js";
2
+ import type { ErrorFormatter } from "./http/error-formatter.js";
3
+ import { type RawHttpOperationHandler } from "./http/raw-operation.js";
4
+ import type { RequestDecoder } from "./http/request-decoder.js";
2
5
  import { RestResource } from "./rest-resource.js";
3
6
  import type { RestResourceProps } from "./rest-resource-operation.js";
4
7
  import { SimResource, type SimResourceProps } from "./resource.js";
8
+ /** Configures behaviour shared by every resource on one simulated API. */
9
+ export interface SimApiProps {
10
+ /** The service name used in route errors. `SimApi` when none is given. */
11
+ name?: string;
12
+ /**
13
+ * How request bodies are read, for the resources that read one.
14
+ *
15
+ * JSON when none is given. A resource or an operation can name its own, and
16
+ * the closest one wins.
17
+ */
18
+ decode?: RequestDecoder;
19
+ /**
20
+ * How a thrown error becomes a response.
21
+ *
22
+ * Runs before the supplied `EntityNotFoundError` and `DuplicateEntityError`
23
+ * mappings, and declining an error with `undefined` leaves it to them.
24
+ */
25
+ formatError?: ErrorFormatter;
26
+ }
5
27
  /** Configures conventional resource state and its HTTP operations. */
6
28
  export interface SimApiResourceProps<T extends object> extends SimResourceProps<T>, RestResourceProps {
7
29
  }
8
- /** Handles a raw HTTP operation after Simnaril has matched its route. */
9
- export type RawHttpOperationHandler = (context: HttpOperationContext) => Promise<Response> | Response;
10
30
  /** Routes HTTP requests to stateful simulated resources. */
11
31
  export declare class SimApi {
12
32
  #private;
33
+ readonly name: string;
34
+ constructor(props?: SimApiProps);
13
35
  /** Adds middleware around every matched operation in this API. */
14
36
  use(middleware: HttpMiddleware): this;
15
37
  /** Creates resource state and exposes its conventional HTTP operations. */
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAEzB,KAAK,gBAAgB,EACtB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAsB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEnE,sEAAsE;AACtE,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM,CACnD,SAAQ,gBAAgB,CAAC,CAAC,CAAC,EAAE,iBAAiB;CAAG;AAEnD,yEAAyE;AACzE,MAAM,MAAM,uBAAuB,GAAG,CACpC,OAAO,EAAE,oBAAoB,KAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;AAElC,4DAA4D;AAC5D,qBAAa,MAAM;;IAIjB,kEAAkE;IAClE,GAAG,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAGpC;IAED,2EAA2E;IAC3E,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAKzE;IAED,4EAA4E;IAC5E,MAAM,CAAC,CAAC,SAAS,MAAM,EACrB,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EACrB,KAAK,EAAE,iBAAiB,GACvB,YAAY,CAAC,CAAC,CAAC,CAejB;IAED,6CAA6C;IAC7C,SAAS,CACP,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,uBAAuB,GAC9B,gBAAgB,CAelB;IAED,yEAAyE;IACzE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAE1C;CAkBF"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEhE,OAAO,EAAsB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQnE,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;OAKG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IAExB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED,sEAAsE;AACtE,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM,CACnD,SAAQ,gBAAgB,CAAC,CAAC,CAAC,EAAE,iBAAiB;CAAG;AAEnD,4DAA4D;AAC5D,qBAAa,MAAM;;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAKtB,YAAY,KAAK,GAAE,WAAgB,EAIlC;IAED,kEAAkE;IAClE,GAAG,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAGpC;IAED,2EAA2E;IAC3E,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAUzE;IAED,4EAA4E;IAC5E,MAAM,CAAC,CAAC,SAAS,MAAM,EACrB,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EACrB,KAAK,EAAE,iBAAiB,GACvB,YAAY,CAAC,CAAC,CAAC,CAgBjB;IAED,6CAA6C;IAC7C,SAAS,CACP,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,uBAAuB,GAC9B,gBAAgB,CAKlB;IAED,yEAAyE;IACzE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAE1C;CAUF"}
package/dist/api.js CHANGED
@@ -1,12 +1,23 @@
1
- import { createRawHttpOperation, operationMiddleware, } from "./http/operation.js";
2
1
  import { OperationRouter } from "./http/operation-router.js";
3
- import { compileRoute, normalizeMethod } from "./http/route.js";
2
+ import { rawOperation, } from "./http/raw-operation.js";
3
+ import { validateResourcePath } from "./http/resource-path.js";
4
4
  import { attachRestResource, RestResource } from "./rest-resource.js";
5
5
  import { SimResource } from "./resource.js";
6
+ const resourcePathShape = (path) => path
7
+ .split("/")
8
+ .map((segment) => (segment.startsWith(":") ? ":" : segment))
9
+ .join("/");
6
10
  /** Routes HTTP requests to stateful simulated resources. */
7
11
  export class SimApi {
12
+ name;
8
13
  #paths = new Set();
9
- #router = new OperationRouter();
14
+ #router;
15
+ #decode;
16
+ constructor(props = {}) {
17
+ this.name = props.name ?? "SimApi";
18
+ this.#decode = props.decode;
19
+ this.#router = new OperationRouter(this.name, props.formatError);
20
+ }
10
21
  /** Adds middleware around every matched operation in this API. */
11
22
  use(middleware) {
12
23
  this.#router.use(middleware);
@@ -14,53 +25,46 @@ export class SimApi {
14
25
  }
15
26
  /** Creates resource state and exposes its conventional HTTP operations. */
16
27
  resource(props) {
17
- const { operations, path, ...stateProps } = props;
18
- const restProps = operations === undefined ? { path } : { operations, path };
28
+ const { decode, itemPath, locate, operations, path, ...stateProps } = props;
29
+ const restProps = {
30
+ path,
31
+ ...(decode === undefined ? {} : { decode }),
32
+ ...(itemPath === undefined ? {} : { itemPath }),
33
+ ...(locate === undefined ? {} : { locate }),
34
+ ...(operations === undefined ? {} : { operations }),
35
+ };
19
36
  return this.expose(new SimResource(stateProps), restProps);
20
37
  }
21
38
  /** Exposes existing resource state through conventional HTTP operations. */
22
39
  expose(state, props) {
23
- this.#validateResourcePath(props.path);
24
- if (this.#paths.has(props.path)) {
40
+ validateResourcePath(props.path);
41
+ const pathShape = resourcePathShape(props.path);
42
+ if (this.#paths.has(pathShape)) {
25
43
  throw new TypeError(`A resource is already exposed at collection path "${props.path}".`);
26
44
  }
27
- const resource = new RestResource(state, props);
45
+ const resource = new RestResource(state, this.#withApiDecode(props));
28
46
  attachRestResource(resource, (operation) => {
29
47
  this.#router.register(operation);
30
48
  });
31
- this.#paths.add(props.path);
49
+ this.#paths.add(pathShape);
32
50
  return resource;
33
51
  }
34
52
  /** Adds a raw HTTP operation to this API. */
35
53
  operation(method, path, handle) {
36
- const route = compileRoute(path);
37
- const operation = createRawHttpOperation();
38
- this.#router.register({
39
- ...route,
40
- decode: () => Promise.resolve(),
41
- encode: (output) => output,
42
- method: normalizeMethod(method),
43
- middleware: operationMiddleware(operation),
44
- operate: (_input, context) => handle(context),
45
- resourceMiddleware: [],
46
- transform: (decoded) => decoded,
47
- });
48
- return operation;
54
+ const built = rawOperation(method, path, handle);
55
+ this.#router.register(built.http);
56
+ return built.operation;
49
57
  }
50
58
  /** Handles one request through the matching simulated HTTP operation. */
51
59
  handle(request) {
52
60
  return this.#router.handle(request);
53
61
  }
54
- #validateResourcePath(path) {
55
- const base = new URL("https://simnaril.invalid");
56
- const url = new URL(path, base);
57
- if (url.origin !== base.origin ||
58
- url.pathname !== path ||
59
- url.search !== "" ||
60
- url.hash !== "" ||
61
- !/^\/[^/:]+(?:\/[^/:]+)*$/u.test(path)) {
62
- throw new TypeError(`Expected a resource collection path such as "/widgets", received "${path}".`);
62
+ /** Applies the API's decoder to a resource that has not named its own. */
63
+ #withApiDecode(props) {
64
+ if (props.decode !== undefined || this.#decode === undefined) {
65
+ return props;
63
66
  }
67
+ return { ...props, decode: this.#decode };
64
68
  }
65
69
  }
66
70
  //# sourceMappingURL=api.js.map
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { RequestDecoder } from "./request-decoder.js";
2
+ /**
3
+ * Reads an `application/x-www-form-urlencoded` body with bracketed nesting.
4
+ *
5
+ * A wire format, and not one service's dialect. Stripe, Rails and PHP
6
+ * applications all speak it, and all three write nesting the same way.
7
+ *
8
+ * ```text
9
+ * line_items[0][price_data][unit_amount]=250
10
+ * ```
11
+ *
12
+ * becomes
13
+ *
14
+ * ```json
15
+ * { "line_items": [{ "price_data": { "unit_amount": "250" } }] }
16
+ * ```
17
+ *
18
+ * Every leaf is a string. A form body carries no types, and guessing at them
19
+ * would make `quantity=1` and `postcode=01234` disagree about what a digit is.
20
+ * The resource's own creation behaviour converts what it needs.
21
+ *
22
+ * The header is ignored. What a body says it is and what it holds are two
23
+ * facts, and a decoder chosen by hand has already settled the first.
24
+ *
25
+ * ## What it refuses
26
+ *
27
+ * A real encoder emits well-formed keys. The cases below are hostile or
28
+ * mistaken input, and each throws a `SyntaxError`, the way a malformed JSON
29
+ * body already does.
30
+ *
31
+ * - A key given twice (`name=a&name=b`). Which one wins is a guess, and losing
32
+ * half a request quietly is worse than stopping.
33
+ * - A key needing one part to hold both a value and more keys (`a=1&a[b]=2`).
34
+ * - An empty bracket anywhere but the end (`a[][b]=1`).
35
+ * - A key brackets cannot be read out of (`a[b`, `[a]`).
36
+ */
37
+ export declare const decodeForm: RequestDecoder;
38
+ //# sourceMappingURL=decode-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-form.d.ts","sourceRoot":"","sources":["../../src/http/decode-form.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,UAAU,EAAE,cASxB,CAAC"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Reads an `application/x-www-form-urlencoded` body with bracketed nesting.
3
+ *
4
+ * A wire format, and not one service's dialect. Stripe, Rails and PHP
5
+ * applications all speak it, and all three write nesting the same way.
6
+ *
7
+ * ```text
8
+ * line_items[0][price_data][unit_amount]=250
9
+ * ```
10
+ *
11
+ * becomes
12
+ *
13
+ * ```json
14
+ * { "line_items": [{ "price_data": { "unit_amount": "250" } }] }
15
+ * ```
16
+ *
17
+ * Every leaf is a string. A form body carries no types, and guessing at them
18
+ * would make `quantity=1` and `postcode=01234` disagree about what a digit is.
19
+ * The resource's own creation behaviour converts what it needs.
20
+ *
21
+ * The header is ignored. What a body says it is and what it holds are two
22
+ * facts, and a decoder chosen by hand has already settled the first.
23
+ *
24
+ * ## What it refuses
25
+ *
26
+ * A real encoder emits well-formed keys. The cases below are hostile or
27
+ * mistaken input, and each throws a `SyntaxError`, the way a malformed JSON
28
+ * body already does.
29
+ *
30
+ * - A key given twice (`name=a&name=b`). Which one wins is a guess, and losing
31
+ * half a request quietly is worse than stopping.
32
+ * - A key needing one part to hold both a value and more keys (`a=1&a[b]=2`).
33
+ * - An empty bracket anywhere but the end (`a[][b]=1`).
34
+ * - A key brackets cannot be read out of (`a[b`, `[a]`).
35
+ */
36
+ export const decodeForm = async (request) => {
37
+ const body = await request.text();
38
+ const root = new Map();
39
+ for (const [key, value] of new URLSearchParams(body)) {
40
+ insert(root, key, value);
41
+ }
42
+ return materialise(root);
43
+ };
44
+ /** A branch of the tree, as opposed to a leaf holding one value. */
45
+ const isBranch = (held) => held instanceof Map;
46
+ /** A key is a name followed by any number of bracketed parts. */
47
+ const keyPattern = /^(?<name>[^[\]]+)(?<parts>(?:\[[^[\]]*\])*)$/u;
48
+ const partPattern = /\[(?<part>[^[\]]*)\]/gu;
49
+ const indexPattern = /^\d+$/u;
50
+ /** The parts of one form key, outermost first. */
51
+ function partsOf(key) {
52
+ const match = keyPattern.exec(key);
53
+ const name = match?.groups?.["name"];
54
+ if (name === undefined) {
55
+ throw new SyntaxError(`Form key "${key}" is not a name followed by bracketed parts.`);
56
+ }
57
+ const parts = [name];
58
+ for (const part of (match?.groups?.["parts"] ?? "").matchAll(partPattern)) {
59
+ parts.push(part.groups?.["part"] ?? "");
60
+ }
61
+ return parts;
62
+ }
63
+ /** Puts one key's value into the tree, growing branches on the way down. */
64
+ function insert(root, key, value) {
65
+ const parts = partsOf(key);
66
+ let node = root;
67
+ for (const [depth, part] of parts.entries()) {
68
+ const last = depth === parts.length - 1;
69
+ if (part === "" && !last) {
70
+ throw new SyntaxError(`Form key "${key}" has an empty bracket before its last part.`);
71
+ }
72
+ // An empty bracket appends, and the position it takes is the branch's size.
73
+ const name = part === "" ? String(node.size) : part;
74
+ if (last) {
75
+ if (node.has(name)) {
76
+ throw new SyntaxError(`Form key "${key}" is given twice.`);
77
+ }
78
+ node.set(name, value);
79
+ return;
80
+ }
81
+ node = branchAt(node, name, key);
82
+ }
83
+ }
84
+ /** The branch below `name`, created when it is the first key to need one. */
85
+ function branchAt(node, name, key) {
86
+ const held = node.get(name);
87
+ if (isBranch(held)) {
88
+ return held;
89
+ }
90
+ if (held !== undefined) {
91
+ throw new SyntaxError(`Form key "${key}" needs "${name}" to hold a value and more keys at once.`);
92
+ }
93
+ const branch = new Map();
94
+ node.set(name, branch);
95
+ return branch;
96
+ }
97
+ /**
98
+ * Turns the tree into arrays and objects.
99
+ *
100
+ * A branch whose every part is a run of digits becomes an array. The digits
101
+ * order the entries and do not position them, so `a[0]`, `a[5]` and `a[9]`
102
+ * give three elements and never a sparse array of ten. A real encoder counts
103
+ * from zero, where the two readings agree.
104
+ */
105
+ function materialise(node) {
106
+ const entries = [...node].map(([name, held]) => [name, isBranch(held) ? materialise(held) : held]);
107
+ if (entries.length > 0 &&
108
+ entries.every(([name]) => indexPattern.test(name))) {
109
+ return entries
110
+ .toSorted(([one], [other]) => Number(one) - Number(other))
111
+ .map(([, held]) => held);
112
+ }
113
+ /*
114
+ * `Object.fromEntries` defines own data properties, so a `__proto__` part of
115
+ * a hostile key lands beside the others and reaches no prototype.
116
+ */
117
+ return Object.fromEntries(entries);
118
+ }
119
+ //# sourceMappingURL=decode-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-form.js","sourceRoot":"","sources":["../../src/http/decode-form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Turns an error thrown by an operation into the response one API answers with.
3
+ *
4
+ * A real service has one error envelope across every endpoint. Stripe's is
5
+ * `{ error: { type, code, message, param } }`, GitHub's is
6
+ * `{ message, documentation_url }`, and simulating either takes describing the
7
+ * shape once.
8
+ *
9
+ * Returning `undefined` declines the error and leaves it to the supplied
10
+ * mappings, so a formatter can shape the errors it knows and ignore the rest.
11
+ */
12
+ export type ErrorFormatter = (error: unknown) => Response | undefined;
13
+ //# sourceMappingURL=error-formatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-formatter.d.ts","sourceRoot":"","sources":["../../src/http/error-formatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,QAAQ,GAAG,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error-formatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-formatter.js","sourceRoot":"","sources":["../../src/http/error-formatter.ts"],"names":[],"mappings":""}
@@ -1,7 +1,9 @@
1
+ import type { ErrorFormatter } from "./error-formatter.js";
1
2
  import type { HttpMiddleware, HttpOperation } from "./operation.js";
2
3
  /** Selects and runs registered operations for one simulated API. */
3
4
  export declare class OperationRouter {
4
5
  #private;
6
+ constructor(apiName: string, formatError?: ErrorFormatter);
5
7
  use(middleware: HttpMiddleware): void;
6
8
  register(operation: HttpOperation): void;
7
9
  handle(request: Request): Promise<Response>;
@@ -1 +1 @@
1
- {"version":3,"file":"operation-router.d.ts","sourceRoot":"","sources":["../../src/http/operation-router.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEd,MAAM,gBAAgB,CAAC;AAIxB,oEAAoE;AACpE,qBAAa,eAAe;;IAI1B,GAAG,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAEpC;IAED,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAEvC;IAEK,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0BhD;CA2CF"}
1
+ {"version":3,"file":"operation-router.d.ts","sourceRoot":"","sources":["../../src/http/operation-router.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EAEd,MAAM,gBAAgB,CAAC;AAIxB,oEAAoE;AACpE,qBAAa,eAAe;;IAM1B,YAAY,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,cAAc,EAGxD;IAED,GAAG,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAEpC;IAED,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAEvC;IAEK,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0BhD;CAsDF"}
@@ -4,8 +4,14 @@ import { UnimplementedRouteError } from "../unimplemented-route-error.js";
4
4
  import { runHttpOperation, runMiddleware } from "./operation-pipeline.js";
5
5
  /** Selects and runs registered operations for one simulated API. */
6
6
  export class OperationRouter {
7
+ #apiName;
7
8
  #middleware = [];
8
9
  #operations = [];
10
+ #formatError;
11
+ constructor(apiName, formatError) {
12
+ this.#apiName = apiName;
13
+ this.#formatError = formatError;
14
+ }
9
15
  use(middleware) {
10
16
  this.#middleware.push(middleware);
11
17
  }
@@ -28,7 +34,7 @@ export class OperationRouter {
28
34
  }
29
35
  }
30
36
  if (selected === undefined) {
31
- throw new UnimplementedRouteError(request);
37
+ throw new UnimplementedRouteError(request, this.#apiName);
32
38
  }
33
39
  return this.#run(selected.operation, request, selected.match);
34
40
  }
@@ -50,6 +56,15 @@ export class OperationRouter {
50
56
  return await action();
51
57
  }
52
58
  catch (error) {
59
+ /*
60
+ * The API's own formatter goes first, so a service that shapes its errors
61
+ * shapes these two as well. Declining one leaves it to the mappings
62
+ * below.
63
+ */
64
+ const formatted = this.#formatError?.(error);
65
+ if (formatted !== undefined) {
66
+ return formatted;
67
+ }
53
68
  if (error instanceof EntityNotFoundError) {
54
69
  return Response.json({ error: error.message }, { status: 404 });
55
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"operation-router.js","sourceRoot":"","sources":["../../src/http/operation-router.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"operation-router.js","sourceRoot":"","sources":["../../src/http/operation-router.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import type { RequestDecoder } from "./request-decoder.js";
1
2
  import type { CompiledRoute, RouteMatch } from "./route.js";
2
3
  /** HTTP data shared by handlers and middleware for one matched operation. */
3
4
  export interface HttpOperationContext {
@@ -17,7 +18,7 @@ export interface SemanticOperationOverride<TInput, TOutput, TResource> {
17
18
  handle: (context: SemanticOperationContext<TInput, TResource>) => Promise<TOutput> | TOutput;
18
19
  }
19
20
  export interface HttpOperation extends CompiledRoute {
20
- decode: (request: Request) => Promise<unknown>;
21
+ decode: RequestDecoder;
21
22
  encode: (output: unknown) => Promise<Response> | Response;
22
23
  method: string;
23
24
  middleware: readonly HttpMiddleware[];
@@ -1 +1 @@
1
- {"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/http/operation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"operation.d.ts","sourceRoot":"","sources":["../../src/http/operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5D,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzC,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8DAA8D;AAC9D,MAAM,MAAM,cAAc,GAAG,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,KAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;AAElC,uEAAuE;AACvE,MAAM,WAAW,wBAAwB,CACvC,MAAM,EACN,SAAS,CACT,SAAQ,oBAAoB;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS;IACnE,MAAM,EAAE,CACN,OAAO,EAAE,wBAAwB,CAAC,MAAM,EAAE,SAAS,CAAC,KACjD,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,aAAc,SAAQ,aAAa;IAClD,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IACtC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC;IACpE,kBAAkB,EAAE,SAAS,cAAc,EAAE,CAAC;IAC9C,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC;CAC7D;AAED,QAAA,MAAM,gBAAgB,eAAiC,CAAC;AACxD,QAAA,MAAM,WAAW,eAAuC,CAAC;AAEzD,gFAAgF;AAChF,qBAAa,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS;;IAMvD,YACE,MAAM,EAAE,CACN,OAAO,EAAE,wBAAwB,CAAC,MAAM,EAAE,SAAS,CAAC,KACjD,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,EAGhC;IAED,oDAAoD;IACpD,QAAQ,CACN,WAAW,EAAE,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,GACjE,IAAI,CAGN;IAED,6CAA6C;IAC7C,GAAG,CAAC,UAAU,EAAE,cAAc,GAAG,IAAI,CAGpC;IAED,CAAC,WAAW,CAAC,CACX,OAAO,EAAE,wBAAwB,CAAC,MAAM,EAAE,SAAS,CAAC,GACnD,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAE5B;IAED,CAAC,gBAAgB,CAAC,IAAI,SAAS,cAAc,EAAE,CAE9C;CACF;AAED,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,GAAG,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,IAAI,CAAC;IAC1C,CAAC,gBAAgB,CAAC,EAAE,MAAM,SAAS,cAAc,EAAE,CAAC;CACrD;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CAUzD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAC5D,SAAS,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,GAC1E,SAAS,cAAc,EAAE,CAE3B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAC7D,SAAS,EAAE,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,EACxD,OAAO,EAAE,wBAAwB,CAAC,MAAM,EAAE,SAAS,CAAC,GACnD,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAE5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"operation.js","sourceRoot":"","sources":["../../src/http/operation.ts"],"names":[],"mappings":"AAyCA,MAAM,gBAAgB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AACxD,MAAM,WAAW,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAEzD,gFAAgF;AAChF,MAAM,OAAO,iBAAiB;IACnB,WAAW,GAAqB,EAAE,CAAC;IAC5C,OAAO,CAEyB;IAEhC,YACE,MAE+B;QAE/B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,oDAAoD;IACpD,QAAQ,CACN,WAAkE;QAElE,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6CAA6C;IAC7C,GAAG,CAAC,UAA0B;QAC5B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,CAAC,WAAW,CAAC,CACX,OAAoD;QAEpD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,CAAC,gBAAgB,CAAC;QAChB,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF;AASD,MAAM,UAAU,sBAAsB;IACpC,MAAM,UAAU,GAAqB,EAAE,CAAC;IAExC,OAAO;QACL,GAAG,CAAC,KAAK;YACP,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,CAAC,gBAAgB,CAAC,EAAE,GAAG,EAAE,CAAC,UAAU;KACrC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,SAA2E;IAE3E,OAAO,SAAS,CAAC,gBAAgB,CAAC,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,SAAwD,EACxD,OAAoD;IAEpD,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
1
+ {"version":3,"file":"operation.js","sourceRoot":"","sources":["../../src/http/operation.ts"],"names":[],"mappings":"AA0CA,MAAM,gBAAgB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AACxD,MAAM,WAAW,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAEzD,gFAAgF;AAChF,MAAM,OAAO,iBAAiB;IACnB,WAAW,GAAqB,EAAE,CAAC;IAC5C,OAAO,CAEyB;IAEhC,YACE,MAE+B;QAE/B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,oDAAoD;IACpD,QAAQ,CACN,WAAkE;QAElE,IAAI,CAAC,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6CAA6C;IAC7C,GAAG,CAAC,UAA0B;QAC5B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,CAAC,WAAW,CAAC,CACX,OAAoD;QAEpD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,CAAC,gBAAgB,CAAC;QAChB,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF;AASD,MAAM,UAAU,sBAAsB;IACpC,MAAM,UAAU,GAAqB,EAAE,CAAC;IAExC,OAAO;QACL,GAAG,CAAC,KAAK;YACP,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,CAAC,gBAAgB,CAAC,EAAE,GAAG,EAAE,CAAC,UAAU;KACrC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,SAA2E;IAE3E,OAAO,SAAS,CAAC,gBAAgB,CAAC,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,SAAwD,EACxD,OAAoD;IAEpD,OAAO,SAAS,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Returns one matched path parameter or throws when it is absent. */
2
+ export declare function requirePathParameter(params: Readonly<Record<string, string>>, name: string): string;
3
+ //# sourceMappingURL=path-parameter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-parameter.d.ts","sourceRoot":"","sources":["../../src/http/path-parameter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACxC,IAAI,EAAE,MAAM,GACX,MAAM,CAYR"}
@@ -0,0 +1,12 @@
1
+ /** Returns one matched path parameter or throws when it is absent. */
2
+ export function requirePathParameter(params, name) {
3
+ if (!Object.hasOwn(params, name)) {
4
+ throw new TypeError(`Matched operation has no ":${name}" path parameter.`);
5
+ }
6
+ const value = params[name];
7
+ if (value === undefined) {
8
+ throw new TypeError(`Matched operation has no ":${name}" path parameter.`);
9
+ }
10
+ return value;
11
+ }
12
+ //# sourceMappingURL=path-parameter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-parameter.js","sourceRoot":"","sources":["../../src/http/path-parameter.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,MAAM,UAAU,oBAAoB,CAClC,MAAwC,EACxC,IAAY;IAEZ,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,8BAA8B,IAAI,mBAAmB,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAE3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,SAAS,CAAC,8BAA8B,IAAI,mBAAmB,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { HttpOperationContext, RawHttpOperation } from "./operation.js";
2
+ import type { HttpOperation } from "./operation.js";
3
+ /** Handles a raw HTTP operation after Simnaril has matched its route. */
4
+ export type RawHttpOperationHandler = (context: HttpOperationContext) => Promise<Response> | Response;
5
+ /**
6
+ * Builds an operation whose handler owns the whole response.
7
+ *
8
+ * The router still supplies the matched path parameters and the query, so a
9
+ * handler down here reimplements no route matching. Everything above that is
10
+ * the handler's, including the status and the body.
11
+ */
12
+ export declare function rawOperation(method: string, path: string, handle: RawHttpOperationHandler): {
13
+ http: HttpOperation;
14
+ operation: RawHttpOperation;
15
+ };
16
+ //# sourceMappingURL=raw-operation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-operation.d.ts","sourceRoot":"","sources":["../../src/http/raw-operation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD,yEAAyE;AACzE,MAAM,MAAM,uBAAuB,GAAG,CACpC,OAAO,EAAE,oBAAoB,KAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;AAElC;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,uBAAuB,GAC9B;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAgBtD"}
@@ -0,0 +1,27 @@
1
+ import { createRawHttpOperation, operationMiddleware } from "./operation.js";
2
+ import { decodeNothing } from "./request-decoder.js";
3
+ import { compileRoute, normalizeMethod } from "./route.js";
4
+ /**
5
+ * Builds an operation whose handler owns the whole response.
6
+ *
7
+ * The router still supplies the matched path parameters and the query, so a
8
+ * handler down here reimplements no route matching. Everything above that is
9
+ * the handler's, including the status and the body.
10
+ */
11
+ export function rawOperation(method, path, handle) {
12
+ const operation = createRawHttpOperation();
13
+ return {
14
+ operation,
15
+ http: {
16
+ ...compileRoute(path),
17
+ decode: decodeNothing,
18
+ encode: (output) => output,
19
+ method: normalizeMethod(method),
20
+ middleware: operationMiddleware(operation),
21
+ operate: (_input, context) => handle(context),
22
+ resourceMiddleware: [],
23
+ transform: (decoded) => decoded,
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=raw-operation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw-operation.js","sourceRoot":"","sources":["../../src/http/raw-operation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAO3D;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,IAAY,EACZ,MAA+B;IAE/B,MAAM,SAAS,GAAG,sBAAsB,EAAE,CAAC;IAE3C,OAAO;QACL,SAAS;QACT,IAAI,EAAE;YACJ,GAAG,YAAY,CAAC,IAAI,CAAC;YACrB,MAAM,EAAE,aAAa;YACrB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAkB;YACtC,MAAM,EAAE,eAAe,CAAC,MAAM,CAAC;YAC/B,UAAU,EAAE,mBAAmB,CAAC,SAAS,CAAC;YAC1C,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC;YAC7C,kBAAkB,EAAE,EAAE;YACtB,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO;SAChC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Reads a request body into the input a semantic operation receives.
3
+ *
4
+ * JSON is the default and stays the default. A decoder is supplied for the
5
+ * services that speak something else on the way in. Stripe, Rails and PHP
6
+ * applications all take `application/x-www-form-urlencoded` and answer with
7
+ * JSON, and simulating one takes this hook.
8
+ *
9
+ * A decoder is only ever asked for a body by an operation that has one to read.
10
+ * `GET` and `DELETE` inherit none, whatever is configured above them.
11
+ */
12
+ export type RequestDecoder = (request: Request) => unknown;
13
+ /** The default decoder, and what an operation uses when none is supplied. */
14
+ export declare const decodeJson: RequestDecoder;
15
+ /**
16
+ * Runs a decoder only when the request arrived with a body.
17
+ *
18
+ * An operation that may be called either way asks for this. Handing the
19
+ * decoder a request with no body is how `decodeJson` comes to answer
20
+ * `Unexpected end of JSON input` for a `DELETE` that was sent nothing.
21
+ */
22
+ export declare const decodeWhenPresent: (decode: RequestDecoder) => RequestDecoder;
23
+ /**
24
+ * Reads nothing at all.
25
+ *
26
+ * What an operation with no body to read uses. Internal, and kept beside the
27
+ * default so the two live together.
28
+ */
29
+ export declare const decodeNothing: () => undefined;
30
+ //# sourceMappingURL=request-decoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-decoder.d.ts","sourceRoot":"","sources":["../../src/http/request-decoder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;AAE3D,6EAA6E;AAC7E,eAAO,MAAM,UAAU,EAAE,cAA4C,CAAC;AAEtE;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,WACnB,cAAc,KAAG,cAE2B,CAAC;AAExD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,QAAO,SAAsB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** The default decoder, and what an operation uses when none is supplied. */
2
+ export const decodeJson = (request) => request.json();
3
+ /**
4
+ * Runs a decoder only when the request arrived with a body.
5
+ *
6
+ * An operation that may be called either way asks for this. Handing the
7
+ * decoder a request with no body is how `decodeJson` comes to answer
8
+ * `Unexpected end of JSON input` for a `DELETE` that was sent nothing.
9
+ */
10
+ export const decodeWhenPresent = (decode) => (request) => request.body === null ? undefined : decode(request);
11
+ /**
12
+ * Reads nothing at all.
13
+ *
14
+ * What an operation with no body to read uses. Internal, and kept beside the
15
+ * default so the two live together.
16
+ */
17
+ export const decodeNothing = () => undefined;
18
+ //# sourceMappingURL=request-decoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-decoder.js","sourceRoot":"","sources":["../../src/http/request-decoder.ts"],"names":[],"mappings":"AAaA,6EAA6E;AAC7E,MAAM,CAAC,MAAM,UAAU,GAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,CAAC,MAAsB,EAAkB,EAAE,CAC3C,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAExD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,GAAc,EAAE,CAAC,SAAS,CAAC"}