@tetsujs/openapi 0.4.1 → 0.5.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/README.md +140 -3
- package/dist/src/annotations.d.ts +40 -0
- package/dist/src/annotations.d.ts.map +1 -1
- package/dist/src/controller.d.ts +32 -9
- package/dist/src/controller.d.ts.map +1 -1
- package/dist/src/document.d.ts +7 -0
- package/dist/src/document.d.ts.map +1 -1
- package/dist/src/emit.d.ts +3 -1
- package/dist/src/emit.d.ts.map +1 -1
- package/dist/src/envelopes.d.ts +72 -0
- package/dist/src/envelopes.d.ts.map +1 -0
- package/dist/src/errors.d.ts +74 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/index.d.ts +31 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +374 -135
- package/dist/src/index.js.map +11 -9
- package/dist/src/json-schema.d.ts +91 -0
- package/dist/src/json-schema.d.ts.map +1 -0
- package/dist/src/operation.d.ts +10 -1
- package/dist/src/operation.d.ts.map +1 -1
- package/dist/src/page.d.ts +20 -0
- package/dist/src/page.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/annotations.ts +43 -0
- package/src/controller.ts +69 -22
- package/src/document.ts +9 -0
- package/src/emit.ts +3 -1
- package/src/envelopes.ts +268 -0
- package/src/errors.ts +177 -0
- package/src/index.ts +52 -6
- package/src/json-schema.ts +108 -0
- package/src/operation.ts +328 -187
- package/src/page.ts +65 -6
package/src/operation.ts
CHANGED
|
@@ -16,17 +16,26 @@ import type {
|
|
|
16
16
|
BodyType,
|
|
17
17
|
RouteTableEntry,
|
|
18
18
|
} from "@tetsujs/core";
|
|
19
|
-
import
|
|
19
|
+
import { errorBody } from "@tetsujs/core";
|
|
20
|
+
import type {
|
|
21
|
+
DocumentedResponse,
|
|
22
|
+
HookContributions,
|
|
23
|
+
SecurityRequirement,
|
|
24
|
+
} from "./annotations.ts";
|
|
20
25
|
import { contributionsOf } from "./annotations.ts";
|
|
21
26
|
import type { SchemaComponents } from "./components.ts";
|
|
22
27
|
import { failureName } from "./components.ts";
|
|
23
28
|
import type {
|
|
24
29
|
ContentMap,
|
|
30
|
+
HeaderObject,
|
|
25
31
|
OperationObject,
|
|
26
32
|
ResponseObject,
|
|
27
33
|
} from "./document.ts";
|
|
28
34
|
import type { JsonSchemaObject } from "./emit.ts";
|
|
29
35
|
import { emitted } from "./emit.ts";
|
|
36
|
+
import type { Envelopes } from "./envelopes.ts";
|
|
37
|
+
import { branchesOf } from "./envelopes.ts";
|
|
38
|
+
import type { JsonSchema } from "./json-schema.ts";
|
|
30
39
|
import { partParameters, pathParameters } from "./parameters.ts";
|
|
31
40
|
|
|
32
41
|
/** Describes one route. */
|
|
@@ -34,6 +43,7 @@ export function operationOf(
|
|
|
34
43
|
entry: RouteTableEntry,
|
|
35
44
|
options: AppOptions,
|
|
36
45
|
components: SchemaComponents,
|
|
46
|
+
envelopes: Envelopes,
|
|
37
47
|
ids: OperationIds,
|
|
38
48
|
warn: (message: string) => void,
|
|
39
49
|
): OperationObject {
|
|
@@ -68,10 +78,85 @@ export function operationOf(
|
|
|
68
78
|
...(contributed.security.length > 0
|
|
69
79
|
? { security: securityOf(contributed.conditions) }
|
|
70
80
|
: {}),
|
|
71
|
-
responses: responses(
|
|
81
|
+
responses: responses(
|
|
82
|
+
entry,
|
|
83
|
+
options,
|
|
84
|
+
contributed,
|
|
85
|
+
components,
|
|
86
|
+
envelopes,
|
|
87
|
+
warn,
|
|
88
|
+
),
|
|
72
89
|
};
|
|
73
90
|
}
|
|
74
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Records the envelopes a route declares, so that its definitions are the
|
|
94
|
+
* ones the document keeps — whichever operation refers to them first.
|
|
95
|
+
*
|
|
96
|
+
* Runs before any operation is built. A schema that cannot describe itself
|
|
97
|
+
* is skipped without a word: building the operation says so, once.
|
|
98
|
+
*/
|
|
99
|
+
export function declareEnvelopes(
|
|
100
|
+
entry: RouteTableEntry,
|
|
101
|
+
envelopes: Envelopes,
|
|
102
|
+
): void {
|
|
103
|
+
for (const [status, schema] of declaredResponses(entry)) {
|
|
104
|
+
const described =
|
|
105
|
+
schema === null ? undefined : emitted(schema, "", () => {});
|
|
106
|
+
|
|
107
|
+
const code = envelopes.format.code;
|
|
108
|
+
|
|
109
|
+
for (const branch of described ? branchesOf(described, code) : []) {
|
|
110
|
+
const found = code(branch);
|
|
111
|
+
|
|
112
|
+
if (found !== undefined) {
|
|
113
|
+
envelopes.declare(Number(status), found, branch);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The route's response map as pairs; a single schema is its `200`. */
|
|
120
|
+
function declaredResponses(
|
|
121
|
+
entry: RouteTableEntry,
|
|
122
|
+
): [status: string, schema: AnySchema | null][] {
|
|
123
|
+
const response = entry.def.schema?.response;
|
|
124
|
+
|
|
125
|
+
if (!response) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if ("~standard" in response) {
|
|
130
|
+
return [["200", response as AnySchema]];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return Object.entries(response) as [string, AnySchema | null][];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A described body as the alternatives the document lists: every envelope
|
|
138
|
+
* in it a reference to the one definition of its status and code, and a
|
|
139
|
+
* union taken apart into its branches, so that it joins the other answers
|
|
140
|
+
* of its status rather than nesting inside them.
|
|
141
|
+
*/
|
|
142
|
+
function branchesFor(
|
|
143
|
+
schema: JsonSchemaObject,
|
|
144
|
+
status: number,
|
|
145
|
+
envelopes: Envelopes,
|
|
146
|
+
warn: (message: string) => void,
|
|
147
|
+
): Record<string, unknown>[] {
|
|
148
|
+
return branchesOf(schema, envelopes.format.code).map((branch) => {
|
|
149
|
+
const code = envelopes.format.code(branch);
|
|
150
|
+
|
|
151
|
+
return code === undefined
|
|
152
|
+
? branch
|
|
153
|
+
: (envelopes.ref(status, code, branch, warn) as unknown as Record<
|
|
154
|
+
string,
|
|
155
|
+
unknown
|
|
156
|
+
>);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
75
160
|
/**
|
|
76
161
|
* The security a route asks for, as OpenAPI spells it.
|
|
77
162
|
*
|
|
@@ -334,90 +419,131 @@ function responses(
|
|
|
334
419
|
options: AppOptions,
|
|
335
420
|
contributed: HookContributions,
|
|
336
421
|
components: SchemaComponents,
|
|
422
|
+
envelopes: Envelopes,
|
|
337
423
|
warn: (message: string) => void,
|
|
338
424
|
): Record<string, ResponseObject> {
|
|
339
|
-
const
|
|
340
|
-
const response = entry.def.schema?.response;
|
|
425
|
+
const answers = new Map<string, Answer[]>();
|
|
341
426
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
427
|
+
const add = (status: string, answer: Answer): void => {
|
|
428
|
+
answers.set(status, [...(answers.get(status) ?? []), answer]);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
for (const [status, schema] of declaredResponses(entry)) {
|
|
432
|
+
const described =
|
|
433
|
+
schema === null ? undefined : emitted(schema, "a response", warn);
|
|
434
|
+
|
|
435
|
+
add(status, {
|
|
436
|
+
description: describeStatus(status),
|
|
437
|
+
placeholder: true,
|
|
438
|
+
schemas: described
|
|
439
|
+
? branchesFor(described, Number(status), envelopes, warn)
|
|
440
|
+
: [],
|
|
441
|
+
});
|
|
355
442
|
}
|
|
356
443
|
|
|
357
|
-
if (!
|
|
358
|
-
|
|
444
|
+
if (![...answers.keys()].some((status) => status.startsWith("2"))) {
|
|
445
|
+
add("200", {
|
|
446
|
+
description: "Successful response",
|
|
447
|
+
placeholder: true,
|
|
448
|
+
schemas: [],
|
|
449
|
+
});
|
|
359
450
|
}
|
|
360
451
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
452
|
+
for (const [status, answer] of failures(
|
|
453
|
+
entry,
|
|
454
|
+
options,
|
|
455
|
+
contributed,
|
|
456
|
+
components,
|
|
457
|
+
envelopes,
|
|
458
|
+
warn,
|
|
459
|
+
)) {
|
|
460
|
+
add(String(status), answer);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const described: Record<string, ResponseObject> = {};
|
|
464
|
+
|
|
465
|
+
for (const [status, list] of answers) {
|
|
466
|
+
described[status] = merge(list, envelopes);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return described;
|
|
372
470
|
}
|
|
373
471
|
|
|
374
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Everything the route can answer with that its own schema does not say:
|
|
474
|
+
* what its hooks declare, and what the framework itself answers.
|
|
475
|
+
*/
|
|
476
|
+
function failures(
|
|
375
477
|
entry: RouteTableEntry,
|
|
376
478
|
options: AppOptions,
|
|
377
479
|
contributed: HookContributions,
|
|
378
|
-
declared: Record<string, ResponseObject>,
|
|
379
480
|
components: SchemaComponents,
|
|
481
|
+
envelopes: Envelopes,
|
|
380
482
|
warn: (message: string) => void,
|
|
381
|
-
):
|
|
483
|
+
): [status: number, answer: Answer][] {
|
|
382
484
|
const schema = entry.def.schema;
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const add = (status: number, failure: Failure): void => {
|
|
386
|
-
const key = String(status);
|
|
485
|
+
const found: [number, Answer][] = [];
|
|
387
486
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
487
|
+
/**
|
|
488
|
+
* Refers to the definition of one failure: the one of its status and
|
|
489
|
+
* code, or — for a failure whose code only the hook knows and did not
|
|
490
|
+
* declare — one of its own, named after the status.
|
|
491
|
+
*/
|
|
392
492
|
const envelopeRef = (
|
|
393
493
|
status: number,
|
|
394
494
|
error?: string,
|
|
395
495
|
message?: string,
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
496
|
+
fields?: DocumentedResponse["fields"],
|
|
497
|
+
): Record<string, unknown> => {
|
|
498
|
+
const described = envelopes.format.describe({
|
|
499
|
+
status,
|
|
500
|
+
...(error === undefined ? {} : { error }),
|
|
501
|
+
...(message === undefined ? {} : { message }),
|
|
502
|
+
fields: fields ?? {},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const ref =
|
|
506
|
+
error === undefined
|
|
507
|
+
? components.ref(failureName(status), described)
|
|
508
|
+
: envelopes.ref(status, error, described, warn);
|
|
509
|
+
|
|
510
|
+
return ref as unknown as Record<string, unknown>;
|
|
511
|
+
};
|
|
401
512
|
|
|
402
513
|
if (schema?.params || schema?.query || schema?.headers || schema?.body) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
514
|
+
found.push([
|
|
515
|
+
options.validationStatus,
|
|
516
|
+
{
|
|
517
|
+
description: "Request failed schema validation",
|
|
518
|
+
schemas: [
|
|
519
|
+
envelopes.ref(
|
|
520
|
+
options.validationStatus,
|
|
521
|
+
"VALIDATION_FAILED",
|
|
522
|
+
envelopes.format.describe({
|
|
523
|
+
status: options.validationStatus,
|
|
524
|
+
error: "VALIDATION_FAILED",
|
|
525
|
+
message: "Validation failed",
|
|
526
|
+
fields: { issues },
|
|
527
|
+
}),
|
|
528
|
+
warn,
|
|
529
|
+
) as unknown as Record<string, unknown>,
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
]);
|
|
410
533
|
}
|
|
411
534
|
|
|
412
535
|
for (const requirement of contributed.security) {
|
|
413
536
|
const status = requirement.status ?? 401;
|
|
414
537
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
538
|
+
found.push([
|
|
539
|
+
status,
|
|
540
|
+
{
|
|
541
|
+
description:
|
|
542
|
+
requirement.description ??
|
|
543
|
+
`Request did not satisfy ${requirement.name}`,
|
|
544
|
+
schemas: [envelopeRef(status, requirement.error, requirement.message)],
|
|
545
|
+
},
|
|
546
|
+
]);
|
|
421
547
|
}
|
|
422
548
|
|
|
423
549
|
for (const response of contributed.responses) {
|
|
@@ -425,52 +551,83 @@ function frameworkFailures(
|
|
|
425
551
|
? emitted(response.schema, "a hook's response", warn)
|
|
426
552
|
: undefined;
|
|
427
553
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
554
|
+
found.push([
|
|
555
|
+
response.status,
|
|
556
|
+
{
|
|
557
|
+
description: response.description,
|
|
558
|
+
schemas: described
|
|
559
|
+
? branchesFor(described, response.status, envelopes, warn)
|
|
560
|
+
: [
|
|
561
|
+
envelopeRef(
|
|
562
|
+
response.status,
|
|
563
|
+
response.error,
|
|
564
|
+
response.message,
|
|
565
|
+
response.fields,
|
|
566
|
+
),
|
|
567
|
+
],
|
|
568
|
+
...(response.headers ? { headers: response.headers } : {}),
|
|
569
|
+
},
|
|
570
|
+
]);
|
|
434
571
|
}
|
|
435
572
|
|
|
436
573
|
if (schema?.body || entry.def.bodyType) {
|
|
437
574
|
const unparsable = parseFailure(entry.def.bodyType);
|
|
438
575
|
|
|
439
576
|
if (unparsable) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
577
|
+
found.push([
|
|
578
|
+
400,
|
|
579
|
+
{
|
|
580
|
+
description: "Body could not be parsed in the declared shape",
|
|
581
|
+
schemas: [envelopeRef(400, unparsable.error, unparsable.message)],
|
|
582
|
+
},
|
|
583
|
+
]);
|
|
444
584
|
}
|
|
445
585
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
586
|
+
found.push([
|
|
587
|
+
413,
|
|
588
|
+
{
|
|
589
|
+
description: "Body exceeded the configured size limit",
|
|
590
|
+
schemas: [
|
|
591
|
+
envelopeRef(
|
|
592
|
+
413,
|
|
593
|
+
"BODY_TOO_LARGE",
|
|
594
|
+
"Body exceeds the configured limit",
|
|
595
|
+
),
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
]);
|
|
454
599
|
}
|
|
455
600
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
601
|
+
found.push([
|
|
602
|
+
500,
|
|
603
|
+
{
|
|
604
|
+
description: "The request failed and nothing mapped the failure",
|
|
605
|
+
schemas: [
|
|
606
|
+
envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error"),
|
|
607
|
+
],
|
|
608
|
+
},
|
|
609
|
+
]);
|
|
466
610
|
|
|
467
|
-
return
|
|
611
|
+
return found;
|
|
468
612
|
}
|
|
469
613
|
|
|
470
|
-
/**
|
|
471
|
-
|
|
614
|
+
/**
|
|
615
|
+
* One way a route can answer with a status: what the route declared, what
|
|
616
|
+
* a hook declared, or what the framework answers by itself.
|
|
617
|
+
*
|
|
618
|
+
* `schemas` are the alternatives of the body, each envelope already a
|
|
619
|
+
* reference to its definition; empty when the answer has no body to
|
|
620
|
+
* describe.
|
|
621
|
+
*
|
|
622
|
+
* A route declares a status by its schema alone and has no words for it,
|
|
623
|
+
* so its description is a `placeholder`: it stands for the status only
|
|
624
|
+
* when nothing else describes it.
|
|
625
|
+
*/
|
|
626
|
+
interface Answer {
|
|
472
627
|
readonly description: string;
|
|
473
|
-
readonly
|
|
628
|
+
readonly placeholder?: boolean;
|
|
629
|
+
readonly schemas: readonly Record<string, unknown>[];
|
|
630
|
+
readonly headers?: Readonly<Record<string, HeaderObject>>;
|
|
474
631
|
}
|
|
475
632
|
|
|
476
633
|
/**
|
|
@@ -487,41 +644,81 @@ interface Failure {
|
|
|
487
644
|
* the answer the endpoint is about, and the failures are what can happen
|
|
488
645
|
* to it. Identical bodies are folded together — a `$ref` repeated is one
|
|
489
646
|
* alternative, not two.
|
|
647
|
+
*
|
|
648
|
+
* Headers are gathered the same way, the first description of a name
|
|
649
|
+
* standing for all of them. Descriptions are joined, except a route's
|
|
650
|
+
* placeholder next to one that says something.
|
|
490
651
|
*/
|
|
491
|
-
function merge(
|
|
492
|
-
|
|
493
|
-
list
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
...declaredSchemas(declared),
|
|
502
|
-
...list.map((failure) => failure.schema),
|
|
503
|
-
]);
|
|
504
|
-
|
|
505
|
-
const [only] = schemas;
|
|
652
|
+
function merge(list: readonly Answer[], envelopes: Envelopes): ResponseObject {
|
|
653
|
+
const schemas = distinct(list.flatMap((answer) => answer.schemas));
|
|
654
|
+
const worded = list.filter((answer) => !answer.placeholder);
|
|
655
|
+
const headers: Record<string, HeaderObject> = {};
|
|
656
|
+
|
|
657
|
+
for (const answer of list) {
|
|
658
|
+
for (const [name, header] of Object.entries(answer.headers ?? {})) {
|
|
659
|
+
headers[name] ??= header;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
506
662
|
|
|
507
663
|
return {
|
|
508
|
-
description:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
664
|
+
description: (worded.length > 0 ? worded : list)
|
|
665
|
+
.map((answer) => answer.description)
|
|
666
|
+
.join("; "),
|
|
667
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
668
|
+
...(schemas.length > 0
|
|
669
|
+
? {
|
|
670
|
+
content: {
|
|
671
|
+
"application/json": { schema: unionOf(schemas, envelopes) },
|
|
672
|
+
},
|
|
673
|
+
}
|
|
674
|
+
: {}),
|
|
512
675
|
};
|
|
513
676
|
}
|
|
514
677
|
|
|
515
678
|
/**
|
|
516
|
-
* The
|
|
517
|
-
*
|
|
679
|
+
* The alternatives of a body as one schema.
|
|
680
|
+
*
|
|
681
|
+
* When every alternative is an envelope, each with its own code, the union
|
|
682
|
+
* says so with a `discriminator` on `error`: a generated client then
|
|
683
|
+
* builds a tagged union and narrows on the code, instead of trying the
|
|
684
|
+
* shapes in turn. The mapping is spelled out, because without it OpenAPI
|
|
685
|
+
* matches the value against the component's name — `Unauthorized`, where
|
|
686
|
+
* the body says `UNAUTHORIZED`. The field is the error format's; a format
|
|
687
|
+
* that names none — its code nested below the top — has no discriminator.
|
|
518
688
|
*/
|
|
519
|
-
function
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
689
|
+
function unionOf(
|
|
690
|
+
schemas: readonly Record<string, unknown>[],
|
|
691
|
+
envelopes: Envelopes,
|
|
692
|
+
): Record<string, unknown> {
|
|
693
|
+
const [only] = schemas;
|
|
523
694
|
|
|
524
|
-
|
|
695
|
+
if (schemas.length === 1 && only) {
|
|
696
|
+
return only;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const propertyName = envelopes.format.discriminator;
|
|
700
|
+
|
|
701
|
+
if (propertyName === undefined) {
|
|
702
|
+
return { anyOf: schemas };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const mapping: Record<string, string> = {};
|
|
706
|
+
|
|
707
|
+
for (const schema of schemas) {
|
|
708
|
+
const ref = (schema as { $ref?: unknown }).$ref;
|
|
709
|
+
const code = typeof ref === "string" ? envelopes.codeOf(ref) : undefined;
|
|
710
|
+
|
|
711
|
+
if (typeof ref !== "string" || code === undefined) {
|
|
712
|
+
return { anyOf: schemas };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
mapping[code] = ref;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
anyOf: schemas,
|
|
720
|
+
discriminator: { propertyName, mapping },
|
|
721
|
+
};
|
|
525
722
|
}
|
|
526
723
|
|
|
527
724
|
function distinct(
|
|
@@ -542,88 +739,32 @@ function distinct(
|
|
|
542
739
|
});
|
|
543
740
|
}
|
|
544
741
|
|
|
545
|
-
function jsonResponse(
|
|
546
|
-
schema: unknown,
|
|
547
|
-
description: string,
|
|
548
|
-
warn: (message: string) => void,
|
|
549
|
-
): ResponseObject {
|
|
550
|
-
const described = emitted(schema, "a response", warn);
|
|
551
|
-
|
|
552
|
-
if (!described) {
|
|
553
|
-
return { description };
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
return {
|
|
557
|
-
description,
|
|
558
|
-
content: { "application/json": { schema: described } },
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function hasSuccess(declared: Record<string, ResponseObject>): boolean {
|
|
563
|
-
return Object.keys(declared).some((status) => status.startsWith("2"));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
742
|
function describeStatus(status: string): string {
|
|
567
743
|
if (status === "204") {
|
|
568
744
|
return "No content";
|
|
569
745
|
}
|
|
570
746
|
|
|
571
|
-
return status.startsWith("2")
|
|
747
|
+
return status.startsWith("2")
|
|
748
|
+
? "Successful response"
|
|
749
|
+
: errorBody(Number(status)).message;
|
|
572
750
|
}
|
|
573
751
|
|
|
574
752
|
/**
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
* which one it will be: a generated client can then discriminate on
|
|
579
|
-
* `error` instead of receiving "an object with three strings", and a
|
|
580
|
-
* reader of the document sees `"BODY_TOO_LARGE"` rather than `string`.
|
|
581
|
-
*
|
|
582
|
-
* The message is documented by example, never as a `const` — it is wording
|
|
583
|
-
* meant for a human, the one field a client must not match on, and the one
|
|
584
|
-
* this project reserves the right to reword.
|
|
753
|
+
* What a validation failure carries besides the code and the message: every
|
|
754
|
+
* issue, with where it is. Handed to the error format as a field, for the
|
|
755
|
+
* format to place.
|
|
585
756
|
*/
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
message?: string,
|
|
590
|
-
): JsonSchemaObject {
|
|
591
|
-
return {
|
|
757
|
+
const issues: JsonSchema = {
|
|
758
|
+
type: "array",
|
|
759
|
+
items: {
|
|
592
760
|
type: "object",
|
|
593
|
-
required: ["
|
|
761
|
+
required: ["message", "path"],
|
|
594
762
|
properties: {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
? { type: "string", examples: [message] }
|
|
598
|
-
: { type: "string" },
|
|
599
|
-
error: error ? { type: "string", const: error } : { type: "string" },
|
|
763
|
+
message: { type: "string" },
|
|
764
|
+
path: { type: "array", items: { type: ["string", "number"] } },
|
|
600
765
|
},
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/** The validation envelope: the standard three fields, plus the issues. */
|
|
605
|
-
function validationFailed(status: number): Record<string, unknown> {
|
|
606
|
-
const base = envelope(status, "VALIDATION_FAILED", "Validation failed");
|
|
607
|
-
|
|
608
|
-
return {
|
|
609
|
-
...base,
|
|
610
|
-
required: ["status", "message", "error", "issues"],
|
|
611
|
-
properties: {
|
|
612
|
-
...(base.properties as Record<string, unknown>),
|
|
613
|
-
issues: {
|
|
614
|
-
type: "array",
|
|
615
|
-
items: {
|
|
616
|
-
type: "object",
|
|
617
|
-
required: ["message", "path"],
|
|
618
|
-
properties: {
|
|
619
|
-
message: { type: "string" },
|
|
620
|
-
path: { type: "array", items: { type: ["string", "number"] } },
|
|
621
|
-
},
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
};
|
|
626
|
-
}
|
|
766
|
+
},
|
|
767
|
+
};
|
|
627
768
|
|
|
628
769
|
/**
|
|
629
770
|
* How a body of this type fails to parse — or nothing, for a text or a
|