• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobinTail / express-zod-api / 7454493116

08 Jan 2024 11:33PM CUT coverage: 100.0%. Remained the same
7454493116

Pull #1464

github

web-flow
Merge b56024dd6 into 65de4b497
Pull Request #1464: Bump @typescript-eslint/parser from 6.18.0 to 6.18.1

676 of 707 branches covered (0.0%)

1166 of 1166 relevant lines covered (100.0%)

908.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/src/documentation-helpers.ts
1
import assert from "node:assert/strict";
2
import {
3
  ContentObject,
4
  ExampleObject,
5
  ExamplesObject,
6
  OAuthFlowObject,
7
  OAuthFlowsObject,
8
  ParameterObject,
9
  ReferenceObject,
10
  RequestBodyObject,
11
  ResponseObject,
12
  SchemaObject,
13
  SchemaObjectType,
14
  SecurityRequirementObject,
15
  SecuritySchemeObject,
16
  TagObject,
17
  isReferenceObject,
18
} from "openapi3-ts/oas31";
19
import { omit } from "ramda";
20
import { z } from "zod";
21
import {
22
  FlatObject,
23
  getExamples,
24
  hasCoercion,
25
  hasRaw,
26
  hasTopLevelTransformingEffect,
27
  isCustomHeader,
28
  makeCleanId,
29
  tryToTransform,
30
  ucFirst,
31
} from "./common-helpers";
32
import { InputSource, TagsConfig } from "./config-type";
33
import { ZodDateIn, isoDateRegex } from "./date-in-schema";
34
import { ZodDateOut } from "./date-out-schema";
35
import { DocumentationError } from "./errors";
36
import { ZodFile } from "./file-schema";
37
import { IOSchema } from "./io-schema";
38
import {
39
  LogicalContainer,
40
  andToOr,
41
  mapLogicalContainer,
42
} from "./logical-container";
43
import { copyMeta } from "./metadata";
44
import { Method } from "./method";
45
import {
46
  HandlingRules,
47
  HandlingVariant,
48
  SchemaHandler,
49
  walkSchema,
50
} from "./schema-walker";
51
import { Security } from "./security";
52
import { ZodUpload } from "./upload-schema";
53

54
/* eslint-disable @typescript-eslint/no-use-before-define */
55

56
export interface OpenAPIContext extends FlatObject {
57
  isResponse: boolean;
58
  serializer: (schema: z.ZodTypeAny) => string;
59
  getRef: (name: string) => ReferenceObject | undefined;
60
  makeRef: (
61
    name: string,
62
    schema: SchemaObject | ReferenceObject,
63
  ) => ReferenceObject;
64
  path: string;
65
  method: Method;
66
}
67

68
type Depicter<
69
  T extends z.ZodTypeAny,
70
  Variant extends HandlingVariant = "regular",
71
> = SchemaHandler<T, SchemaObject | ReferenceObject, OpenAPIContext, Variant>;
72

73
interface ReqResDepictHelperCommonProps
74
  extends Pick<
75
    OpenAPIContext,
76
    "serializer" | "getRef" | "makeRef" | "path" | "method"
77
  > {
78
  schema: z.ZodTypeAny;
79
  mimeTypes: string[];
80
  composition: "inline" | "components";
81
  description?: string;
82
}
83

84
const shortDescriptionLimit = 50;
240✔
85
const isoDateDocumentationUrl =
86
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
240✔
87

88
const samples = {
240✔
89
  integer: 0,
90
  number: 0,
91
  string: "",
92
  boolean: false,
93
  object: {},
94
  null: null,
95
  array: [],
96
} satisfies Record<Extract<SchemaObjectType, string>, unknown>;
97

98
/** @see https://expressjs.com/en/guide/routing.html */
99
const routePathParamsRegex = /:([A-Za-z0-9_]+)/g;
240✔
100

101
export const getRoutePathParams = (path: string): string[] => {
240✔
102
  const match = path.match(routePathParamsRegex);
1,416✔
103
  if (!match) {
1,416✔
104
    return [];
1,164✔
105
  }
106
  return match.map((param) => param.slice(1));
312✔
107
};
108

109
export const reformatParamsInPath = (path: string) =>
240✔
110
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
744✔
111

112
export const depictDefault: Depicter<z.ZodDefault<z.ZodTypeAny>> = ({
240✔
113
  schema: {
114
    _def: { innerType, defaultValue },
115
  },
116
  next,
117
}) => ({
36✔
118
  ...next({ schema: innerType }),
119
  default: defaultValue(),
120
});
121

122
export const depictCatch: Depicter<z.ZodCatch<z.ZodTypeAny>> = ({
240✔
123
  schema: {
124
    _def: { innerType },
125
  },
126
  next,
127
}) => next({ schema: innerType });
12✔
128

129
export const depictAny: Depicter<z.ZodAny> = () => ({
240✔
130
  format: "any",
131
});
132

133
export const depictUpload: Depicter<ZodUpload> = (ctx) => {
240✔
134
  assert(
48✔
135
    !ctx.isResponse,
136
    new DocumentationError({
137
      message: "Please use ez.upload() only for input.",
138
      ...ctx,
139
    }),
140
  );
141
  return {
36✔
142
    type: "string",
143
    format: "binary",
144
  };
145
};
146

147
export const depictFile: Depicter<ZodFile> = ({
240✔
148
  schema: { isBinary, isBase64, isBuffer },
149
}) => ({
84✔
150
  type: "string",
151
  format: isBuffer || isBinary ? "binary" : isBase64 ? "byte" : "file",
228✔
152
});
153

154
export const depictUnion: Depicter<z.ZodUnion<z.ZodUnionOptions>> = ({
240✔
155
  schema: { options },
156
  next,
157
}) => ({
156✔
158
  oneOf: options.map((option) => next({ schema: option })),
312✔
159
});
160

161
export const depictDiscriminatedUnion: Depicter<
162
  z.ZodDiscriminatedUnion<string, z.ZodDiscriminatedUnionOption<string>[]>
163
> = ({ schema: { options, discriminator }, next }) => {
240✔
164
  return {
36✔
165
    discriminator: { propertyName: discriminator },
166
    oneOf: Array.from(options.values()).map((option) =>
167
      next({ schema: option }),
72✔
168
    ),
169
  };
170
};
171

172
export const depictIntersection: Depicter<
173
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
174
> = ({
240✔
175
  schema: {
176
    _def: { left, right },
177
  },
178
  next,
179
}) => ({
108✔
180
  allOf: [left, right].map((entry) => next({ schema: entry })),
216✔
181
});
182

183
export const depictOptional: Depicter<z.ZodOptional<z.ZodTypeAny>> = ({
240✔
184
  schema,
185
  next,
186
}) => next({ schema: schema.unwrap() });
132✔
187

188
export const depictReadonly: Depicter<z.ZodReadonly<z.ZodTypeAny>> = ({
240✔
189
  schema,
190
  next,
191
}) => next({ schema: schema._def.innerType });
24✔
192

193
/** @since OAS 3.1 nullable replaced with type array having null */
194
export const depictNullable: Depicter<z.ZodNullable<z.ZodTypeAny>> = ({
240✔
195
  schema,
196
  next,
197
}) => {
198
  const nested = next({ schema: schema.unwrap() });
120✔
199
  if (!isReferenceObject(nested)) {
120!
200
    nested.type = makeNullableType(nested);
120✔
201
  }
202
  return nested;
120✔
203
};
204

205
export const depictEnum: Depicter<
206
  z.ZodEnum<[string, ...string[]]> | z.ZodNativeEnum<any> // keeping "any" for ZodNativeEnum as compatibility fix
207
> = ({ schema }) => ({
240✔
208
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
209
  enum: Object.values(schema.enum),
210
});
211

212
export const depictLiteral: Depicter<z.ZodLiteral<unknown>> = ({
240✔
213
  schema: { value },
214
}) => ({
1,788✔
215
  type: typeof value as "string" | "number" | "boolean",
216
  enum: [value],
217
});
218

219
export const depictObject: Depicter<z.AnyZodObject> = ({
240✔
220
  schema,
221
  isResponse,
222
  ...rest
223
}) => {
224
  const required = Object.keys(schema.shape).filter((key) => {
3,828✔
225
    const prop = schema.shape[key];
5,940✔
226
    const isOptional =
227
      isResponse && hasCoercion(prop)
5,940✔
228
        ? prop instanceof z.ZodOptional
229
        : prop.isOptional();
230
    return !isOptional;
5,940✔
231
  });
232
  const result: SchemaObject = {
3,828✔
233
    type: "object",
234
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
235
  };
236
  if (required.length) {
3,744✔
237
    result.required = required;
3,372✔
238
  }
239
  return result;
3,744✔
240
};
241

242
/**
243
 * @see https://swagger.io/docs/specification/data-models/data-types/
244
 * @since OAS 3.1: using type: "null"
245
 * */
246
export const depictNull: Depicter<z.ZodNull> = () => ({ type: "null" });
240✔
247

248
export const depictDateIn: Depicter<ZodDateIn> = (ctx) => {
240✔
249
  assert(
60✔
250
    !ctx.isResponse,
251
    new DocumentationError({
252
      message: "Please use ez.dateOut() for output.",
253
      ...ctx,
254
    }),
255
  );
256
  return {
48✔
257
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
258
    type: "string",
259
    format: "date-time",
260
    pattern: isoDateRegex.source,
261
    externalDocs: {
262
      url: isoDateDocumentationUrl,
263
    },
264
  };
265
};
266

267
export const depictDateOut: Depicter<ZodDateOut> = (ctx) => {
240✔
268
  assert(
60✔
269
    ctx.isResponse,
270
    new DocumentationError({
271
      message: "Please use ez.dateIn() for input.",
272
      ...ctx,
273
    }),
274
  );
275
  return {
48✔
276
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
277
    type: "string",
278
    format: "date-time",
279
    externalDocs: {
280
      url: isoDateDocumentationUrl,
281
    },
282
  };
283
};
284

285
/** @throws DocumentationError */
286
export const depictDate: Depicter<z.ZodDate> = (ctx) =>
240✔
287
  assert.fail(
24✔
288
    new DocumentationError({
289
      message: `Using z.date() within ${
290
        ctx.isResponse ? "output" : "input"
24✔
291
      } schema is forbidden. Please use ez.date${
292
        ctx.isResponse ? "Out" : "In"
24✔
293
      }() instead. Check out the documentation for details.`,
294
      ...ctx,
295
    }),
296
  );
297

298
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
396✔
299
  type: "boolean",
300
});
301

302
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
240✔
303
  type: "integer",
304
  format: "bigint",
305
});
306

307
const areOptionsLiteral = (
240✔
308
  subject: z.ZodTypeAny[],
309
): subject is z.ZodLiteral<unknown>[] =>
310
  subject.reduce(
24✔
311
    (carry, option) => carry && option instanceof z.ZodLiteral,
48✔
312
    true,
313
  );
314

315
export const depictRecord: Depicter<z.ZodRecord<z.ZodTypeAny>> = ({
240✔
316
  schema: { keySchema, valueSchema },
317
  ...rest
318
}) => {
319
  if (keySchema instanceof z.ZodEnum || keySchema instanceof z.ZodNativeEnum) {
168✔
320
    const keys = Object.values(keySchema.enum) as string[];
24✔
321
    const shape = keys.reduce<z.ZodRawShape>(
24✔
322
      (carry, key) => ({
48✔
323
        ...carry,
324
        [key]: valueSchema,
325
      }),
326
      {},
327
    );
328
    const result: SchemaObject = {
24✔
329
      type: "object",
330
      properties: depictObjectProperties({
331
        schema: z.object(shape),
332
        ...rest,
333
      }),
334
    };
335
    if (keys.length) {
24!
336
      result.required = keys;
24✔
337
    }
338
    return result;
24✔
339
  }
340
  if (keySchema instanceof z.ZodLiteral) {
144✔
341
    return {
24✔
342
      type: "object",
343
      properties: depictObjectProperties({
344
        schema: z.object({
345
          [keySchema.value]: valueSchema,
346
        }),
347
        ...rest,
348
      }),
349
      required: [keySchema.value],
350
    };
351
  }
352
  if (keySchema instanceof z.ZodUnion) {
120✔
353
    if (areOptionsLiteral(keySchema.options)) {
24!
354
      const shape = keySchema.options.reduce<z.ZodRawShape>(
24✔
355
        (carry, option) => ({
48✔
356
          ...carry,
357
          [`${option.value}`]: valueSchema,
358
        }),
359
        {},
360
      );
361
      return {
24✔
362
        type: "object",
363
        properties: depictObjectProperties({
364
          schema: z.object(shape),
365
          ...rest,
366
        }),
367
        required: keySchema.options.map((option) => option.value),
48✔
368
      };
369
    }
370
  }
371
  return {
96✔
372
    type: "object",
373
    additionalProperties: rest.next({ schema: valueSchema }),
374
  };
375
};
376

377
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
240✔
378
  schema: { _def: def, element },
379
  next,
380
}) => {
381
  const result: SchemaObject = {
180✔
382
    type: "array",
383
    items: next({ schema: element }),
384
  };
385
  if (def.minLength) {
180✔
386
    result.minItems = def.minLength.value;
60✔
387
  }
388
  if (def.maxLength) {
180✔
389
    result.maxItems = def.maxLength.value;
12✔
390
  }
391
  return result;
180✔
392
};
393

394
/** @since OAS 3.1 using prefixItems for depicting tuples */
395
export const depictTuple: Depicter<z.ZodTuple> = ({
240✔
396
  schema: { items },
397
  next,
398
}) => {
399
  const types = items.map((item) => next({ schema: item }));
108✔
400
  return {
60✔
401
    type: "array",
402
    prefixItems: types,
403
  };
404
};
405

406
export const depictString: Depicter<z.ZodString> = ({
240✔
407
  schema: {
408
    isEmail,
409
    isURL,
410
    minLength,
411
    maxLength,
412
    isUUID,
413
    isCUID,
414
    isCUID2,
415
    isULID,
416
    isIP,
417
    isEmoji,
418
    isDatetime,
419
    _def: { checks },
420
  },
421
}) => {
422
  const regexCheck = checks.find(
2,484✔
423
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
424
      check.kind === "regex",
504✔
425
  );
426
  const datetimeCheck = checks.find(
2,484✔
427
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
428
      check.kind === "datetime",
516✔
429
  );
430
  const regex = regexCheck
2,484✔
431
    ? regexCheck.regex
432
    : datetimeCheck
2,376✔
433
      ? datetimeCheck.offset
24✔
434
        ? new RegExp(
435
            `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`,
436
          )
437
        : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
438
      : undefined;
439
  const result: SchemaObject = { type: "string" };
2,484✔
440
  const formats: Record<NonNullable<SchemaObject["format"]>, boolean> = {
2,484✔
441
    "date-time": isDatetime,
442
    email: isEmail,
443
    url: isURL,
444
    uuid: isUUID,
445
    cuid: isCUID,
446
    cuid2: isCUID2,
447
    ulid: isULID,
448
    ip: isIP,
449
    emoji: isEmoji,
450
  };
451
  for (const format in formats) {
2,484✔
452
    if (formats[format]) {
21,480✔
453
      result.format = format;
180✔
454
      break;
180✔
455
    }
456
  }
457
  if (minLength !== null) {
2,484✔
458
    result.minLength = minLength;
144✔
459
  }
460
  if (maxLength !== null) {
2,484✔
461
    result.maxLength = maxLength;
48✔
462
  }
463
  if (regex) {
2,484✔
464
    result.pattern = regex.source;
132✔
465
  }
466
  return result;
2,484✔
467
};
468

469
/** @since OAS 3.1: exclusive min/max are numbers */
470
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
240✔
471
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
684✔
472
    | Extract<z.ZodNumberCheck, { kind: "min" }>
473
    | undefined;
474
  const minimum =
475
    schema.minValue === null
624✔
476
      ? schema.isInt
324✔
477
        ? Number.MIN_SAFE_INTEGER
478
        : Number.MIN_VALUE
479
      : schema.minValue;
480
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
624✔
481
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
732✔
482
    | Extract<z.ZodNumberCheck, { kind: "max" }>
483
    | undefined;
484
  const maximum =
485
    schema.maxValue === null
624✔
486
      ? schema.isInt
540✔
487
        ? Number.MAX_SAFE_INTEGER
488
        : Number.MAX_VALUE
489
      : schema.maxValue;
490
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
624✔
491
  const result: SchemaObject = {
624✔
492
    type: schema.isInt ? "integer" : "number",
624✔
493
    format: schema.isInt ? "int64" : "double",
624✔
494
  };
495
  if (isMinInclusive) {
624✔
496
    result.minimum = minimum;
468✔
497
  } else {
498
    result.exclusiveMinimum = minimum;
156✔
499
  }
500
  if (isMaxInclusive) {
624✔
501
    result.maximum = maximum;
588✔
502
  } else {
503
    result.exclusiveMaximum = maximum;
36✔
504
  }
505
  return result;
624✔
506
};
507

508
export const depictObjectProperties = ({
240✔
509
  schema: { shape },
510
  next,
511
}: Parameters<Depicter<z.AnyZodObject>>[0]) =>
512
  Object.keys(shape).reduce<Record<string, SchemaObject | ReferenceObject>>(
3,912✔
513
    (carry, key) => ({
6,084✔
514
      ...carry,
515
      [key]: next({ schema: shape[key] }),
516
    }),
517
    {},
518
  );
519

520
const makeSample = (depicted: SchemaObject) => {
240✔
521
  const type = (
522
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
84!
523
  ) as keyof typeof samples;
524
  return samples?.[type];
84✔
525
};
526

527
const makeNullableType = (prev: SchemaObject): SchemaObjectType[] => {
240✔
528
  const current = typeof prev.type === "string" ? [prev.type] : prev.type || [];
276!
529
  if (current.includes("null")) {
276✔
530
    return current;
144✔
531
  }
532
  return current.concat("null");
132✔
533
};
534

535
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
240✔
536
  schema,
537
  isResponse,
538
  next,
539
}) => {
540
  const input = next({ schema: schema.innerType() });
396✔
541
  const { effect } = schema._def;
396✔
542
  if (isResponse && effect.type === "transform" && !isReferenceObject(input)) {
396✔
543
    const outputType = tryToTransform(schema, makeSample(input));
84✔
544
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
84✔
545
      return { type: outputType as "number" | "string" | "boolean" };
60✔
546
    } else {
547
      return next({ schema: z.any() });
24✔
548
    }
549
  }
550
  if (
312✔
551
    !isResponse &&
648✔
552
    effect.type === "preprocess" &&
553
    !isReferenceObject(input)
554
  ) {
555
    const { type: inputType, ...rest } = input;
36✔
556
    return {
36✔
557
      ...rest,
558
      format: `${rest.format || inputType} (preprocessed)`,
60✔
559
    };
560
  }
561
  return input;
276✔
562
};
563

564
export const depictPipeline: Depicter<
565
  z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>
566
> = ({ schema, isResponse, next }) =>
240✔
567
  next({ schema: schema._def[isResponse ? "out" : "in"] });
24✔
568

569
export const depictBranded: Depicter<
570
  z.ZodBranded<z.ZodTypeAny, string | number | symbol>
571
> = ({ schema, next }) => next({ schema: schema.unwrap() });
240✔
572

573
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
240✔
574
  next,
575
  schema: lazy,
576
  serializer: serialize,
577
  getRef,
578
  makeRef,
579
}): ReferenceObject => {
580
  const hash = serialize(lazy.schema);
144✔
581
  return (
144✔
582
    getRef(hash) ||
216✔
583
    (() => {
584
      makeRef(hash, {}); // make empty ref first
72✔
585
      return makeRef(hash, next({ schema: lazy.schema })); // update
72✔
586
    })()
587
  );
588
};
589

590
export const depictExamples = (
240✔
591
  schema: z.ZodTypeAny,
592
  isResponse: boolean,
593
  omitProps: string[] = [],
1,656✔
594
): ExamplesObject | undefined => {
595
  const examples = getExamples({
2,052✔
596
    schema,
597
    variant: isResponse ? "parsed" : "original",
2,052✔
598
    validate: true,
599
  });
600
  if (examples.length === 0) {
2,052✔
601
    return undefined;
1,188✔
602
  }
603
  return examples.reduce<ExamplesObject>(
864✔
604
    (carry, example, index) => ({
900✔
605
      ...carry,
606
      [`example${index + 1}`]: {
607
        value:
608
          typeof example === "object" && !Array.isArray(example)
2,676✔
609
            ? omit(omitProps, example)
610
            : example,
611
      } satisfies ExampleObject,
612
    }),
613
    {},
614
  );
615
};
616

617
export const depictParamExamples = (
240✔
618
  schema: z.ZodTypeAny,
619
  isResponse: boolean,
620
  param: string,
621
): ExamplesObject | undefined => {
622
  const examples = getExamples({
480✔
623
    schema,
624
    variant: isResponse ? "parsed" : "original",
480✔
625
    validate: true,
626
  });
627
  if (examples.length === 0) {
480✔
628
    return undefined;
420✔
629
  }
630
  return examples.reduce<ExamplesObject>(
60✔
631
    (carry, example, index) =>
632
      param in example
84!
633
        ? {
634
            ...carry,
635
            [`example${index + 1}`]: {
636
              value: example[param],
637
            } satisfies ExampleObject,
638
          }
639
        : carry,
640
    {},
641
  );
642
};
643

644
export const extractObjectSchema = (
240✔
645
  subject: IOSchema,
646
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">,
647
) => {
648
  if (subject instanceof z.ZodObject) {
1,296✔
649
    return subject;
1,092✔
650
  }
651
  let objectSchema: z.AnyZodObject;
652
  if (
204✔
653
    subject instanceof z.ZodUnion ||
372✔
654
    subject instanceof z.ZodDiscriminatedUnion
655
  ) {
656
    objectSchema = Array.from(subject.options.values())
48✔
657
      .map((option) => extractObjectSchema(option, ctx))
96✔
658
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
96✔
659
  } else if (subject instanceof z.ZodEffects) {
156✔
660
    assert(
36✔
661
      !hasTopLevelTransformingEffect(subject),
662
      new DocumentationError({
663
        message: `Using transformations on the top level of ${
664
          ctx.isResponse ? "response" : "input"
36!
665
        } schema is not allowed.`,
666
        ...ctx,
667
      }),
668
    );
669
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
24✔
670
  } else {
671
    // intersection
672
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
120✔
673
      extractObjectSchema(subject._def.right, ctx),
674
    );
675
  }
676
  return copyMeta(subject, objectSchema);
192✔
677
};
678

679
export const depictRequestParams = ({
240✔
680
  path,
681
  method,
682
  schema,
683
  inputSources,
684
  serializer,
685
  getRef,
686
  makeRef,
687
  composition,
688
  description = `${method.toUpperCase()} ${path} Parameter`,
804✔
689
}: Omit<ReqResDepictHelperCommonProps, "mimeTypes"> & {
690
  inputSources: InputSource[];
691
}): ParameterObject[] => {
692
  const shape = extractObjectSchema(schema, {
828✔
693
    path,
694
    method,
695
    isResponse: false,
696
  }).shape;
697
  const pathParams = getRoutePathParams(path);
828✔
698
  const isQueryEnabled = inputSources.includes("query");
828✔
699
  const areParamsEnabled = inputSources.includes("params");
828✔
700
  const areHeadersEnabled = inputSources.includes("headers");
828✔
701
  const isPathParam = (name: string) =>
828✔
702
    areParamsEnabled && pathParams.includes(name);
1,392✔
703
  const isHeaderParam = (name: string) =>
828✔
704
    areHeadersEnabled && isCustomHeader(name);
348✔
705
  return Object.keys(shape)
828✔
706
    .filter((name) => isQueryEnabled || isPathParam(name))
1,320✔
707
    .map((name) => {
708
      const depicted = walkSchema({
456✔
709
        schema: shape[name],
710
        isResponse: false,
711
        rules: depicters,
712
        onEach,
713
        onMissing,
714
        serializer,
715
        getRef,
716
        makeRef,
717
        path,
718
        method,
719
      });
720
      const result =
721
        composition === "components"
456✔
722
          ? makeRef(makeCleanId(description, name), depicted)
723
          : depicted;
724
      return {
456✔
725
        name,
726
        in: isPathParam(name)
456✔
727
          ? "path"
728
          : isHeaderParam(name)
348✔
729
            ? "header"
730
            : "query",
731
        required: !shape[name].isOptional(),
732
        description: depicted.description || description,
864✔
733
        schema: result,
734
        examples: depictParamExamples(schema, false, name),
735
      };
736
    });
737
};
738

739
export const depicters: HandlingRules<
740
  SchemaObject | ReferenceObject,
741
  OpenAPIContext
742
> = {
240✔
743
  ZodString: depictString,
744
  ZodNumber: depictNumber,
745
  ZodBigInt: depictBigInt,
746
  ZodBoolean: depictBoolean,
747
  ZodDateIn: depictDateIn,
748
  ZodDateOut: depictDateOut,
749
  ZodNull: depictNull,
750
  ZodArray: depictArray,
751
  ZodTuple: depictTuple,
752
  ZodRecord: depictRecord,
753
  ZodObject: depictObject,
754
  ZodLiteral: depictLiteral,
755
  ZodIntersection: depictIntersection,
756
  ZodUnion: depictUnion,
757
  ZodFile: depictFile,
758
  ZodUpload: depictUpload,
759
  ZodAny: depictAny,
760
  ZodDefault: depictDefault,
761
  ZodEnum: depictEnum,
762
  ZodNativeEnum: depictEnum,
763
  ZodEffects: depictEffect,
764
  ZodOptional: depictOptional,
765
  ZodNullable: depictNullable,
766
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
767
  ZodBranded: depictBranded,
768
  ZodDate: depictDate,
769
  ZodCatch: depictCatch,
770
  ZodPipeline: depictPipeline,
771
  ZodLazy: depictLazy,
772
  ZodReadonly: depictReadonly,
773
};
774

775
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
240✔
776
  schema,
777
  isResponse,
778
  prev,
779
}) => {
780
  if (isReferenceObject(prev)) {
10,356✔
781
    return {};
108✔
782
  }
783
  const { description } = schema;
10,248✔
784
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
10,248✔
785
  const hasTypePropertyInDepiction = prev.type !== undefined;
10,248✔
786
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
10,248✔
787
  const isActuallyNullable =
788
    !shouldAvoidParsing &&
10,248✔
789
    hasTypePropertyInDepiction &&
790
    !isResponseHavingCoercion &&
791
    schema.isNullable();
792
  const examples = shouldAvoidParsing
10,248!
793
    ? []
794
    : getExamples({
795
        schema,
796
        variant: isResponse ? "parsed" : "original",
10,248✔
797
        validate: true,
798
      });
799
  const result: SchemaObject = {};
10,248✔
800
  if (description) {
10,248✔
801
    result.description = description;
60✔
802
  }
803
  if (isActuallyNullable) {
10,248✔
804
    result.type = makeNullableType(prev);
156✔
805
  }
806
  if (examples.length) {
10,248✔
807
    result.examples = Array.from(examples);
1,008✔
808
  }
809
  return result;
10,248✔
810
};
811

812
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({ schema, ...ctx }) =>
240✔
813
  assert.fail(
84✔
814
    new DocumentationError({
815
      message: `Zod type ${schema.constructor.name} is unsupported.`,
816
      ...ctx,
817
    }),
818
  );
819

820
export const excludeParamsFromDepiction = (
240✔
821
  depicted: SchemaObject | ReferenceObject,
822
  pathParams: string[],
823
): SchemaObject | ReferenceObject => {
824
  if (isReferenceObject(depicted)) {
636✔
825
    return depicted;
12✔
826
  }
827
  const properties = depicted.properties
624✔
828
    ? omit(pathParams, depicted.properties)
829
    : undefined;
830
  const examples = depicted.examples
624✔
831
    ? depicted.examples.map((entry) => omit(pathParams, entry))
132✔
832
    : undefined;
833
  const required = depicted.required
624✔
834
    ? depicted.required.filter((name) => !pathParams.includes(name))
864✔
835
    : undefined;
836
  const allOf = depicted.allOf
624✔
837
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
838
        excludeParamsFromDepiction(entry, pathParams),
144✔
839
      )
840
    : undefined;
841
  const oneOf = depicted.oneOf
624✔
842
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
843
        excludeParamsFromDepiction(entry, pathParams),
72✔
844
      )
845
    : undefined;
846

847
  return omit(
624✔
848
    Object.entries({ properties, required, examples, allOf, oneOf })
849
      .filter(([{}, value]) => value === undefined)
3,120✔
850
      .map(([key]) => key),
1,968✔
851
    {
852
      ...depicted,
853
      properties,
854
      required,
855
      examples,
856
      allOf,
857
      oneOf,
858
    },
859
  );
860
};
861

862
export const excludeExamplesFromDepiction = (
240✔
863
  depicted: SchemaObject | ReferenceObject,
864
): SchemaObject | ReferenceObject =>
865
  isReferenceObject(depicted) ? depicted : omit(["examples"], depicted);
2,040!
866

867
export const depictResponse = ({
240✔
868
  method,
869
  path,
870
  schema,
871
  mimeTypes,
872
  variant,
873
  serializer,
874
  getRef,
875
  makeRef,
876
  composition,
877
  hasMultipleStatusCodes,
878
  statusCode,
879
  description = `${method.toUpperCase()} ${path} ${ucFirst(variant)} response ${
1,608✔
880
    hasMultipleStatusCodes ? statusCode : ""
1,608✔
881
  }`.trim(),
882
}: ReqResDepictHelperCommonProps & {
883
  variant: "positive" | "negative";
884
  statusCode: number;
885
  hasMultipleStatusCodes: boolean;
886
}): ResponseObject => {
887
  const depictedSchema = excludeExamplesFromDepiction(
1,656✔
888
    walkSchema({
889
      schema,
890
      isResponse: true,
891
      rules: depicters,
892
      onEach,
893
      onMissing,
894
      serializer,
895
      getRef,
896
      makeRef,
897
      path,
898
      method,
899
    }),
900
  );
901
  const examples = depictExamples(schema, true);
1,656✔
902
  const result =
903
    composition === "components"
1,656✔
904
      ? makeRef(makeCleanId(description), depictedSchema)
905
      : depictedSchema;
906

907
  return {
1,656✔
908
    description,
909
    content: mimeTypes.reduce<ContentObject>(
910
      (carry, mimeType) => ({
1,668✔
911
        ...carry,
912
        [mimeType]: { schema: result, examples },
913
      }),
914
      {},
915
    ),
916
  };
917
};
918

919
type SecurityHelper<K extends Security["type"]> = (
920
  security: Security & { type: K },
921
  inputSources?: InputSource[],
922
) => SecuritySchemeObject;
923

924
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
240✔
925
  type: "http",
926
  scheme: "basic",
927
});
928
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
240✔
929
  format: bearerFormat,
930
}) => {
931
  const result: SecuritySchemeObject = {
48✔
932
    type: "http",
933
    scheme: "bearer",
934
  };
935
  if (bearerFormat) {
48✔
936
    result.bearerFormat = bearerFormat;
12✔
937
  }
938
  return result;
48✔
939
};
940
const depictInputSecurity: SecurityHelper<"input"> = (
240✔
941
  { name },
942
  inputSources,
943
) => {
944
  const result: SecuritySchemeObject = {
72✔
945
    type: "apiKey",
946
    in: "query",
947
    name,
948
  };
949
  if (inputSources?.includes("body")) {
72✔
950
    if (inputSources?.includes("query")) {
48✔
951
      result["x-in-alternative"] = "body";
12✔
952
      result.description = `${name} CAN also be supplied within the request body`;
12✔
953
    } else {
954
      result["x-in-actual"] = "body";
36✔
955
      result.description = `${name} MUST be supplied within the request body instead of query`;
36✔
956
    }
957
  }
958
  return result;
72✔
959
};
960
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
240✔
961
  type: "apiKey",
962
  in: "header",
963
  name,
964
});
965
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
240✔
966
  type: "apiKey",
967
  in: "cookie",
968
  name,
969
});
970
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
240✔
971
  url: openIdConnectUrl,
972
}) => ({
12✔
973
  type: "openIdConnect",
974
  openIdConnectUrl,
975
});
976
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
240✔
977
  type: "oauth2",
978
  flows: (
979
    Object.keys(flows) as (keyof typeof flows)[]
980
  ).reduce<OAuthFlowsObject>((acc, key) => {
981
    const flow = flows[key];
84✔
982
    if (!flow) {
84✔
983
      return acc;
24✔
984
    }
985
    const { scopes = {}, ...rest } = flow;
60!
986
    return { ...acc, [key]: { ...rest, scopes } satisfies OAuthFlowObject };
60✔
987
  }, {}),
988
});
989

990
export const depictSecurity = (
240✔
991
  container: LogicalContainer<Security>,
992
  inputSources?: InputSource[],
993
): LogicalContainer<SecuritySchemeObject> => {
994
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
780✔
995
    basic: depictBasicSecurity,
996
    bearer: depictBearerSecurity,
997
    input: depictInputSecurity,
998
    header: depictHeaderSecurity,
999
    cookie: depictCookieSecurity,
1000
    openid: depictOpenIdSecurity,
1001
    oauth2: depictOAuth2Security,
1002
  };
1003
  return mapLogicalContainer(container, (security) =>
780✔
1004
    (methods[security.type] as SecurityHelper<typeof security.type>)(
240✔
1005
      security,
1006
      inputSources,
1007
    ),
1008
  );
1009
};
1010

1011
export const depictSecurityRefs = (
240✔
1012
  container: LogicalContainer<{ name: string; scopes: string[] }>,
1013
): SecurityRequirementObject[] => {
1014
  if (typeof container === "object") {
1,488!
1015
    if ("or" in container) {
1,488✔
1016
      return container.or.map((entry) =>
768✔
1017
        ("and" in entry
216✔
1018
          ? entry.and
1019
          : [entry]
1020
        ).reduce<SecurityRequirementObject>(
1021
          (agg, { name, scopes }) => ({
312✔
1022
            ...agg,
1023
            [name]: scopes,
1024
          }),
1025
          {},
1026
        ),
1027
      );
1028
    }
1029
    if ("and" in container) {
720✔
1030
      return depictSecurityRefs(andToOr(container));
708✔
1031
    }
1032
  }
1033
  return depictSecurityRefs({ or: [container] });
12✔
1034
};
1035

1036
export const depictRequest = ({
240✔
1037
  method,
1038
  path,
1039
  schema,
1040
  mimeTypes,
1041
  serializer,
1042
  getRef,
1043
  makeRef,
1044
  composition,
1045
  description = `${method.toUpperCase()} ${path} Request body`,
432✔
1046
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
1047
  const pathParams = getRoutePathParams(path);
456✔
1048
  const bodyDepiction = excludeExamplesFromDepiction(
456✔
1049
    excludeParamsFromDepiction(
1050
      walkSchema({
1051
        schema: hasRaw(schema) ? ZodFile.create().buffer() : schema,
456✔
1052
        isResponse: false,
1053
        rules: depicters,
1054
        onEach,
1055
        onMissing,
1056
        serializer,
1057
        getRef,
1058
        makeRef,
1059
        path,
1060
        method,
1061
      }),
1062
      pathParams,
1063
    ),
1064
  );
1065
  const bodyExamples = depictExamples(schema, false, pathParams);
372✔
1066
  const result =
1067
    composition === "components"
372✔
1068
      ? makeRef(makeCleanId(description), bodyDepiction)
1069
      : bodyDepiction;
1070

1071
  return {
372✔
1072
    description,
1073
    content: mimeTypes.reduce<ContentObject>(
1074
      (carry, mimeType) => ({
372✔
1075
        ...carry,
1076
        [mimeType]: { schema: result, examples: bodyExamples },
1077
      }),
1078
      {},
1079
    ),
1080
  };
1081
};
1082

1083
export const depictTags = <TAG extends string>(
240✔
1084
  tags: TagsConfig<TAG>,
1085
): TagObject[] =>
1086
  (Object.keys(tags) as TAG[]).map((tag) => {
48✔
1087
    const def = tags[tag];
96✔
1088
    const result: TagObject = {
96✔
1089
      name: tag,
1090
      description: typeof def === "string" ? def : def.description,
96✔
1091
    };
1092
    if (typeof def === "object" && def.url) {
96✔
1093
      result.externalDocs = { url: def.url };
12✔
1094
    }
1095
    return result;
96✔
1096
  });
1097

1098
export const ensureShortDescription = (description: string) => {
240✔
1099
  if (description.length <= shortDescriptionLimit) {
240✔
1100
    return description;
204✔
1101
  }
1102
  return description.slice(0, shortDescriptionLimit - 1) + "…";
36✔
1103
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc