@tetsujs/openapi 0.4.2 → 0.5.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/dist/src/index.js CHANGED
@@ -103,7 +103,7 @@ function failureName(status, error) {
103
103
  }
104
104
 
105
105
  // packages/openapi/src/envelopes.ts
106
- function envelopes(components) {
106
+ function envelopes(components, format) {
107
107
  const declared = new Map;
108
108
  const defined = new Map;
109
109
  const refs = new Map;
@@ -111,6 +111,7 @@ function envelopes(components) {
111
111
  const names = new Map;
112
112
  const reported = new Set;
113
113
  return {
114
+ format,
114
115
  declare(status, code, schema) {
115
116
  const key = keyOf(status, code);
116
117
  if (!declared.has(key)) {
@@ -149,31 +150,18 @@ function envelopes(components) {
149
150
  return `${name}${status}`;
150
151
  }
151
152
  }
152
- function envelopeCode(schema) {
153
- const required = Array.isArray(schema.required) ? schema.required : [];
154
- if (!required.includes("error")) {
155
- return;
156
- }
157
- const error = schema.properties?.error;
158
- if (typeof error?.const === "string") {
159
- return error.const;
160
- }
161
- if (Array.isArray(error?.enum) && error.enum.length === 1 && typeof error.enum[0] === "string") {
162
- return error.enum[0];
163
- }
164
- return;
165
- }
166
- function branchesOf(schema) {
153
+ function branchesOf(schema, code) {
167
154
  const keys = Object.keys(schema);
168
155
  if (keys.length !== 1) {
169
156
  return [schema];
170
157
  }
158
+ const within = (branch) => branchesOf(branch, code);
171
159
  if (Array.isArray(schema.anyOf)) {
172
- return schema.anyOf.flatMap(branchesOf);
160
+ return schema.anyOf.flatMap(within);
173
161
  }
174
162
  if (Array.isArray(schema.oneOf)) {
175
- const branches = schema.oneOf.flatMap(branchesOf);
176
- const codes = branches.map(envelopeCode);
163
+ const branches = schema.oneOf.flatMap(within);
164
+ const codes = branches.map(code);
177
165
  if (codes.every((code) => code !== undefined) && new Set(codes).size === codes.length) {
178
166
  return branches;
179
167
  }
@@ -184,17 +172,89 @@ function keyOf(status, code) {
184
172
  return `${status} ${code}`;
185
173
  }
186
174
  function differenceOf(kept, other) {
187
- const fields = (schema) => Object.keys(schema.properties ?? {});
188
- const keptFields = fields(kept);
189
- const otherFields = fields(other);
190
- const missing = otherFields.filter((name) => !keptFields.includes(name));
191
- const extra = keptFields.filter((name) => !otherFields.includes(name));
192
- const changed = [...missing, ...extra];
175
+ const keptShape = shapeOf(kept);
176
+ const otherShape = shapeOf(other);
177
+ const changed = [
178
+ ...otherShape.fields.filter((path) => !keptShape.fields.includes(path)),
179
+ ...keptShape.fields.filter((path) => !otherShape.fields.includes(path))
180
+ ];
193
181
  if (changed.length > 0) {
194
182
  return `fields: ${changed.join(", ")}`;
195
183
  }
196
- const required = (schema) => JSON.stringify([...Array.isArray(schema.required) ? schema.required : []].sort());
197
- return required(kept) === required(other) ? undefined : "which fields are required";
184
+ return JSON.stringify(keptShape.required) === JSON.stringify(otherShape.required) ? undefined : "which fields are required";
185
+ }
186
+ var shapeDepth = 8;
187
+ function shapeOf(schema) {
188
+ const fields = [];
189
+ const required = [];
190
+ const walk = (node, prefix, depth) => {
191
+ const properties = node.properties;
192
+ if (depth > shapeDepth || properties === null || typeof properties !== "object") {
193
+ return;
194
+ }
195
+ const requiredHere = Array.isArray(node.required) ? node.required : [];
196
+ for (const [name, child] of Object.entries(properties)) {
197
+ const path = `${prefix}${name}`;
198
+ fields.push(path);
199
+ if (requiredHere.includes(name)) {
200
+ required.push(path);
201
+ }
202
+ if (child !== null && typeof child === "object") {
203
+ walk(child, `${path}.`, depth + 1);
204
+ }
205
+ }
206
+ };
207
+ walk(schema, "", 0);
208
+ return { fields: fields.sort(), required: required.sort() };
209
+ }
210
+
211
+ // packages/openapi/src/errors.ts
212
+ function resolveFormat(format) {
213
+ if (format === undefined) {
214
+ return {
215
+ describe: envelope,
216
+ code: codeIn("error"),
217
+ discriminator: "error"
218
+ };
219
+ }
220
+ const discriminator = format.discriminator;
221
+ const read = format.code;
222
+ return {
223
+ describe: (failure) => format.schema(failure),
224
+ code: read ? (schema) => read(schema) : discriminator === undefined ? () => {
225
+ return;
226
+ } : codeIn(discriminator),
227
+ discriminator
228
+ };
229
+ }
230
+ function envelope(failure) {
231
+ const own = {
232
+ status: { type: "integer", const: failure.status },
233
+ message: failure.message ? { type: "string", examples: [failure.message] } : { type: "string" },
234
+ error: failure.error ? { type: "string", const: failure.error } : { type: "string" }
235
+ };
236
+ const added = Object.keys(failure.fields).filter((name) => !Object.hasOwn(own, name));
237
+ return {
238
+ type: "object",
239
+ required: ["status", "message", "error", ...added],
240
+ properties: { ...failure.fields, ...own }
241
+ };
242
+ }
243
+ function codeIn(field) {
244
+ return (schema) => {
245
+ const required = Array.isArray(schema.required) ? schema.required : [];
246
+ if (!required.includes(field)) {
247
+ return;
248
+ }
249
+ const value = schema.properties?.[field];
250
+ if (typeof value?.const === "string") {
251
+ return value.const;
252
+ }
253
+ if (Array.isArray(value?.enum) && value.enum.length === 1 && typeof value.enum[0] === "string") {
254
+ return value.enum[0];
255
+ }
256
+ return;
257
+ };
198
258
  }
199
259
 
200
260
  // packages/openapi/src/operation.ts
@@ -315,10 +375,11 @@ function operationOf(entry, options, components, envelopes, ids, warn) {
315
375
  function declareEnvelopes(entry, envelopes) {
316
376
  for (const [status, schema] of declaredResponses(entry)) {
317
377
  const described = schema === null ? undefined : emitted(schema, "", () => {});
318
- for (const branch of described ? branchesOf(described) : []) {
319
- const code = envelopeCode(branch);
320
- if (code !== undefined) {
321
- envelopes.declare(Number(status), code, branch);
378
+ const code = envelopes.format.code;
379
+ for (const branch of described ? branchesOf(described, code) : []) {
380
+ const found = code(branch);
381
+ if (found !== undefined) {
382
+ envelopes.declare(Number(status), found, branch);
322
383
  }
323
384
  }
324
385
  }
@@ -334,8 +395,8 @@ function declaredResponses(entry) {
334
395
  return Object.entries(response);
335
396
  }
336
397
  function branchesFor(schema, status, envelopes, warn) {
337
- return branchesOf(schema).map((branch) => {
338
- const code = envelopeCode(branch);
398
+ return branchesOf(schema, envelopes.format.code).map((branch) => {
399
+ const code = envelopes.format.code(branch);
339
400
  return code === undefined ? branch : envelopes.ref(status, code, branch, warn);
340
401
  });
341
402
  }
@@ -450,17 +511,13 @@ function responses(entry, options, contributed, components, envelopes, warn) {
450
511
  for (const [status, schema] of declaredResponses(entry)) {
451
512
  const described = schema === null ? undefined : emitted(schema, "a response", warn);
452
513
  add(status, {
453
- description: describeStatus(status),
454
- placeholder: true,
455
- schemas: described ? branchesFor(described, Number(status), envelopes, warn) : []
514
+ placeholder: describeStatus(status),
515
+ said: described ? wordsOf(described, envelopes) : [],
516
+ schemas: described ? branchesFor(unionOnly(described), Number(status), envelopes, warn) : []
456
517
  });
457
518
  }
458
519
  if (![...answers.keys()].some((status) => status.startsWith("2"))) {
459
- add("200", {
460
- description: "Successful response",
461
- placeholder: true,
462
- schemas: []
463
- });
520
+ add("200", { placeholder: "Successful response", said: [], schemas: [] });
464
521
  }
465
522
  for (const [status, answer] of failures(entry, options, contributed, components, envelopes, warn)) {
466
523
  add(String(status), answer);
@@ -475,7 +532,12 @@ function failures(entry, options, contributed, components, envelopes, warn) {
475
532
  const schema = entry.def.schema;
476
533
  const found = [];
477
534
  const envelopeRef = (status, error, message, fields) => {
478
- const described = envelope(status, error, message, fields);
535
+ const described = envelopes.format.describe({
536
+ status,
537
+ ...error === undefined ? {} : { error },
538
+ ...message === undefined ? {} : { message },
539
+ fields: fields ?? {}
540
+ });
479
541
  const ref = error === undefined ? components.ref(failureName(status), described) : envelopes.ref(status, error, described, warn);
480
542
  return ref;
481
543
  };
@@ -483,9 +545,19 @@ function failures(entry, options, contributed, components, envelopes, warn) {
483
545
  found.push([
484
546
  options.validationStatus,
485
547
  {
486
- description: "Request failed schema validation",
548
+ said: [
549
+ {
550
+ code: "VALIDATION_FAILED",
551
+ text: "Request failed schema validation"
552
+ }
553
+ ],
487
554
  schemas: [
488
- envelopes.ref(options.validationStatus, "VALIDATION_FAILED", validationFailed(options.validationStatus), warn)
555
+ envelopes.ref(options.validationStatus, "VALIDATION_FAILED", envelopes.format.describe({
556
+ status: options.validationStatus,
557
+ error: "VALIDATION_FAILED",
558
+ message: "Validation failed",
559
+ fields: { issues }
560
+ }), warn)
489
561
  ]
490
562
  }
491
563
  ]);
@@ -495,7 +567,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
495
567
  found.push([
496
568
  status,
497
569
  {
498
- description: requirement.description ?? `Request did not satisfy ${requirement.name}`,
570
+ said: [
571
+ said(requirement.error, requirement.description ?? `Request did not satisfy ${requirement.name}`)
572
+ ],
499
573
  schemas: [envelopeRef(status, requirement.error, requirement.message)]
500
574
  }
501
575
  ]);
@@ -505,7 +579,7 @@ function failures(entry, options, contributed, components, envelopes, warn) {
505
579
  found.push([
506
580
  response.status,
507
581
  {
508
- description: response.description,
582
+ said: [said(response.error, response.description)],
509
583
  schemas: described ? branchesFor(described, response.status, envelopes, warn) : [
510
584
  envelopeRef(response.status, response.error, response.message, response.fields)
511
585
  ],
@@ -519,7 +593,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
519
593
  found.push([
520
594
  400,
521
595
  {
522
- description: "Body could not be parsed in the declared shape",
596
+ said: [
597
+ said(unparsable.error, "Body could not be parsed in the declared shape")
598
+ ],
523
599
  schemas: [envelopeRef(400, unparsable.error, unparsable.message)]
524
600
  }
525
601
  ]);
@@ -527,7 +603,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
527
603
  found.push([
528
604
  413,
529
605
  {
530
- description: "Body exceeded the configured size limit",
606
+ said: [
607
+ said("BODY_TOO_LARGE", "Body exceeded the configured size limit")
608
+ ],
531
609
  schemas: [
532
610
  envelopeRef(413, "BODY_TOO_LARGE", "Body exceeds the configured limit")
533
611
  ]
@@ -537,7 +615,9 @@ function failures(entry, options, contributed, components, envelopes, warn) {
537
615
  found.push([
538
616
  500,
539
617
  {
540
- description: "The request failed and nothing mapped the failure",
618
+ said: [
619
+ said("INTERNAL_SERVER_ERROR", "The request failed and nothing mapped the failure")
620
+ ],
541
621
  schemas: [
542
622
  envelopeRef(500, "INTERNAL_SERVER_ERROR", "Internal Server Error")
543
623
  ]
@@ -545,9 +625,24 @@ function failures(entry, options, contributed, components, envelopes, warn) {
545
625
  ]);
546
626
  return found;
547
627
  }
628
+ function said(code, text) {
629
+ return code === undefined ? { text } : { code, text };
630
+ }
631
+ function wordsOf(schema, envelopes) {
632
+ if (typeof schema.description === "string") {
633
+ return [said(envelopes.format.code(schema), schema.description)];
634
+ }
635
+ return branchesOf(unionOnly(schema), envelopes.format.code).flatMap((branch) => typeof branch.description === "string" ? [said(envelopes.format.code(branch), branch.description)] : []);
636
+ }
637
+ function unionOnly(schema) {
638
+ if (typeof schema.description !== "string" || !Array.isArray(schema.anyOf) && !Array.isArray(schema.oneOf)) {
639
+ return schema;
640
+ }
641
+ const { description: _, ...union } = schema;
642
+ return union;
643
+ }
548
644
  function merge(list, envelopes) {
549
645
  const schemas = distinct(list.flatMap((answer) => answer.schemas));
550
- const worded = list.filter((answer) => !answer.placeholder);
551
646
  const headers = {};
552
647
  for (const answer of list) {
553
648
  for (const [name, header] of Object.entries(answer.headers ?? {})) {
@@ -555,7 +650,7 @@ function merge(list, envelopes) {
555
650
  }
556
651
  }
557
652
  return {
558
- description: (worded.length > 0 ? worded : list).map((answer) => answer.description).join("; "),
653
+ description: wording(list),
559
654
  ...Object.keys(headers).length > 0 ? { headers } : {},
560
655
  ...schemas.length > 0 ? {
561
656
  content: {
@@ -564,11 +659,38 @@ function merge(list, envelopes) {
564
659
  } : {}
565
660
  };
566
661
  }
662
+ function wording(list) {
663
+ const seen = new Set;
664
+ const items = [];
665
+ for (const answer of list) {
666
+ for (const item of answer.said) {
667
+ const key = `${item.code ?? ""}
668
+ ${item.text}`;
669
+ if (!seen.has(key)) {
670
+ seen.add(key);
671
+ items.push(item);
672
+ }
673
+ }
674
+ }
675
+ const [only] = items;
676
+ if (items.length === 1 && only) {
677
+ return only.text;
678
+ }
679
+ if (items.length > 1) {
680
+ return items.map((item) => item.code === undefined ? `- ${item.text}` : `- \`${item.code}\`: ${item.text}`).join(`
681
+ `);
682
+ }
683
+ return list.find((answer) => answer.placeholder)?.placeholder ?? "";
684
+ }
567
685
  function unionOf(schemas, envelopes) {
568
686
  const [only] = schemas;
569
687
  if (schemas.length === 1 && only) {
570
688
  return only;
571
689
  }
690
+ const propertyName = envelopes.format.discriminator;
691
+ if (propertyName === undefined) {
692
+ return { anyOf: schemas };
693
+ }
572
694
  const mapping = {};
573
695
  for (const schema of schemas) {
574
696
  const ref = schema.$ref;
@@ -580,7 +702,7 @@ function unionOf(schemas, envelopes) {
580
702
  }
581
703
  return {
582
704
  anyOf: schemas,
583
- discriminator: { propertyName: "error", mapping }
705
+ discriminator: { propertyName, mapping }
584
706
  };
585
707
  }
586
708
  function distinct(schemas) {
@@ -600,40 +722,17 @@ function describeStatus(status) {
600
722
  }
601
723
  return status.startsWith("2") ? "Successful response" : errorBody(Number(status)).message;
602
724
  }
603
- function envelope(status, error, message, fields = {}) {
604
- const own = {
605
- status: { type: "integer", const: status },
606
- message: message ? { type: "string", examples: [message] } : { type: "string" },
607
- error: error ? { type: "string", const: error } : { type: "string" }
608
- };
609
- const added = Object.keys(fields).filter((name) => !Object.hasOwn(own, name));
610
- return {
725
+ var issues = {
726
+ type: "array",
727
+ items: {
611
728
  type: "object",
612
- required: ["status", "message", "error", ...added],
613
- properties: { ...fields, ...own }
614
- };
615
- }
616
- function validationFailed(status) {
617
- const base = envelope(status, "VALIDATION_FAILED", "Validation failed");
618
- return {
619
- ...base,
620
- required: ["status", "message", "error", "issues"],
729
+ required: ["message", "path"],
621
730
  properties: {
622
- ...base.properties,
623
- issues: {
624
- type: "array",
625
- items: {
626
- type: "object",
627
- required: ["message", "path"],
628
- properties: {
629
- message: { type: "string" },
630
- path: { type: "array", items: { type: ["string", "number"] } }
631
- }
632
- }
633
- }
731
+ message: { type: "string" },
732
+ path: { type: "array", items: { type: ["string", "number"] } }
634
733
  }
635
- };
636
- }
734
+ }
735
+ };
637
736
  function parseFailure(bodyType) {
638
737
  if (bodyType === "text" || bodyType === "stream") {
639
738
  return;
@@ -751,7 +850,8 @@ class DocsController {
751
850
  [onMount](app) {
752
851
  const generated = openapi(app, {
753
852
  info: this.options.info,
754
- ...this.options.servers ? { servers: this.options.servers } : {}
853
+ ...this.options.servers ? { servers: this.options.servers } : {},
854
+ ...this.options.errors ? { errors: this.options.errors } : {}
755
855
  });
756
856
  this.document = generated.document;
757
857
  const ui = this.options.ui ?? "scalar";
@@ -784,7 +884,7 @@ function openapi(app, options) {
784
884
  const warnings = [];
785
885
  const schemes = {};
786
886
  const components = schemaComponents();
787
- const envelopes2 = envelopes(components);
887
+ const envelopes2 = envelopes(components, resolveFormat(options.errors));
788
888
  const ids = operationIds();
789
889
  const described = app.entries.filter((entry) => !entry.ws && !entry.def.docs?.hidden);
790
890
  for (const entry of described) {
@@ -851,5 +951,5 @@ export {
851
951
  secured
852
952
  };
853
953
 
854
- //# debugId=8D8E4C09AE6F825C64756E2164756E21
954
+ //# debugId=4A68DE756C1883B864756E2164756E21
855
955
  //# sourceMappingURL=index.js.map