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

RobinTail / express-zod-api / 6750739124

03 Nov 2023 10:20PM UTC coverage: 100.0%. First build
6750739124

Pull #1274

github

web-flow
Merge 969648e0c into 64845e53b
Pull Request #1274: Bump @typescript-eslint/eslint-plugin from 6.9.0 to 6.9.1

543 of 566 branches covered (0.0%)

1179 of 1179 relevant lines covered (100.0%)

331.14 hits per line

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

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

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

55
type MediaExamples = Pick<MediaTypeObject, "examples">;
56

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

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

74
interface ReqResDepictHelperCommonProps
75
  extends Pick<
76
    OpenAPIContext,
77
    "serializer" | "getRef" | "makeRef" | "path" | "method"
78
  > {
79
  endpoint: AbstractEndpoint;
80
  composition: "inline" | "components";
81
  clue?: string;
82
}
83

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

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

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

101
export const getRoutePathParams = (path: string): string[] => {
100✔
102
  const match = path.match(routePathParamsRegex);
525✔
103
  if (!match) {
525✔
104
    return [];
440✔
105
  }
106
  return match.map((param) => param.slice(1));
110✔
107
};
108

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

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

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

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

133
export const depictUpload: Depicter<ZodUpload> = (ctx) => {
100✔
134
  if (ctx.isResponse) {
20✔
135
    throw new DocumentationError({
5✔
136
      message: "Please use z.upload() only for input.",
137
      ...ctx,
138
    });
139
  }
140
  return {
15✔
141
    type: "string",
142
    format: "binary",
143
  };
144
};
145

146
export const depictFile: Depicter<ZodFile> = ({
100✔
147
  schema: { isBinary, isBase64 },
148
  ...ctx
149
}) => {
150
  if (!ctx.isResponse) {
30✔
151
    throw new DocumentationError({
5✔
152
      message: "Please use z.file() only within ResultHandler.",
153
      ...ctx,
154
    });
155
  }
156
  return {
25✔
157
    type: "string",
158
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
35✔
159
  };
160
};
161

162
export const depictUnion: Depicter<z.ZodUnion<z.ZodUnionOptions>> = ({
100✔
163
  schema: { options },
164
  next,
165
}) => ({
45✔
166
  oneOf: options.map((option) => next({ schema: option })),
90✔
167
});
168

169
export const depictDiscriminatedUnion: Depicter<
5✔
170
  z.ZodDiscriminatedUnion<string, z.ZodDiscriminatedUnionOption<string>[]>
171
> = ({ schema: { options, discriminator }, next }) => {
100✔
172
  return {
15✔
173
    discriminator: { propertyName: discriminator },
174
    oneOf: Array.from(options.values()).map((option) =>
175
      next({ schema: option }),
30✔
176
    ),
177
  };
178
};
179

180
export const depictIntersection: Depicter<
5✔
181
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
182
> = ({
100✔
183
  schema: {
184
    _def: { left, right },
185
  },
186
  next,
187
}) => ({
45✔
188
  allOf: [left, right].map((entry) => next({ schema: entry })),
90✔
189
});
190

191
export const depictOptional: Depicter<z.ZodOptional<z.ZodTypeAny>> = ({
100✔
192
  schema,
193
  next,
194
}) => next({ schema: schema.unwrap() });
55✔
195

196
export const depictReadonly: Depicter<z.ZodReadonly<z.ZodTypeAny>> = ({
100✔
197
  schema,
198
  next,
199
}) => next({ schema: schema._def.innerType });
10✔
200

201
export const depictNullable: Depicter<z.ZodNullable<z.ZodTypeAny>> = ({
100✔
202
  schema,
203
  next,
204
}) => ({
30✔
205
  nullable: true,
206
  ...next({ schema: schema.unwrap() }),
207
});
208

209
export const depictEnum: Depicter<
10✔
210
  z.ZodEnum<[string, ...string[]]> | z.ZodNativeEnum<any> // keeping "any" for ZodNativeEnum as compatibility fix
211
> = ({ schema }) => ({
100✔
212
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
213
  enum: Object.values(schema.enum),
214
});
215

216
export const depictLiteral: Depicter<z.ZodLiteral<unknown>> = ({
100✔
217
  schema: { value },
218
}) => ({
595✔
219
  type: typeof value as "string" | "number" | "boolean",
220
  enum: [value],
221
});
222

223
export const depictObject: Depicter<z.AnyZodObject> = ({
100✔
224
  schema,
225
  isResponse,
226
  ...rest
227
}) => {
228
  const required = Object.keys(schema.shape).filter((key) => {
1,400✔
229
    const prop = schema.shape[key];
2,180✔
230
    const isOptional =
231
      isResponse && hasCoercion(prop)
2,180✔
232
        ? prop instanceof z.ZodOptional
233
        : prop.isOptional();
234
    return !isOptional;
2,180✔
235
  });
236
  return {
1,400✔
237
    type: "object",
238
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
239
    ...(required.length ? { required } : {}),
1,365✔
240
  };
241
};
242

243
/**
244
 * @see https://swagger.io/docs/specification/data-models/data-types/
245
 * @todo use type:"null" for OpenAPI 3.1
246
 * */
247
export const depictNull: Depicter<z.ZodNull> = () => ({
100✔
248
  type: "string",
249
  nullable: true,
250
  format: "null",
251
});
252

253
export const depictDateIn: Depicter<ZodDateIn> = (ctx) => {
100✔
254
  if (ctx.isResponse) {
25✔
255
    throw new DocumentationError({
5✔
256
      message: "Please use z.dateOut() for output.",
257
      ...ctx,
258
    });
259
  }
260
  return {
20✔
261
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
262
    type: "string",
263
    format: "date-time",
264
    pattern: isoDateRegex.source,
265
    externalDocs: {
266
      url: isoDateDocumentationUrl,
267
    },
268
  };
269
};
270

271
export const depictDateOut: Depicter<ZodDateOut> = (ctx) => {
100✔
272
  if (!ctx.isResponse) {
25✔
273
    throw new DocumentationError({
5✔
274
      message: "Please use z.dateIn() for input.",
275
      ...ctx,
276
    });
277
  }
278
  return {
20✔
279
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
280
    type: "string",
281
    format: "date-time",
282
    externalDocs: {
283
      url: isoDateDocumentationUrl,
284
    },
285
  };
286
};
287

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

300
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
155✔
301
  type: "boolean",
302
});
303

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

309
const areOptionsLiteral = (
100✔
310
  subject: z.ZodTypeAny[],
311
): subject is z.ZodLiteral<unknown>[] =>
312
  subject.reduce(
10✔
313
    (carry, option) => carry && option instanceof z.ZodLiteral,
20✔
314
    true,
315
  );
316

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

376
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
100✔
377
  schema: { _def: def, element },
378
  next,
379
}) => ({
75✔
380
  type: "array",
381
  items: next({ schema: element }),
382
  ...(def.minLength !== null && { minItems: def.minLength.value }),
100✔
383
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
80✔
384
});
385

386
/** @todo improve it when OpenAPI 3.1.0 will be released */
387
export const depictTuple: Depicter<z.ZodTuple> = ({
100✔
388
  schema: { items },
389
  next,
390
}) => {
391
  const types = items.map((item) => next({ schema: item }));
45✔
392
  return {
25✔
393
    type: "array",
394
    minItems: types.length,
395
    maxItems: types.length,
396
    items: {
397
      oneOf: types,
398
      format: "tuple",
399
      ...(types.length > 0 && {
45✔
400
        description: types
401
          .map(
402
            (item, index) =>
403
              `${index}: ${isSchemaObject(item) ? item.type : item.$ref}`,
45!
404
          )
405
          .join(", "),
406
      }),
407
    },
408
  };
409
};
410

411
export const depictString: Depicter<z.ZodString> = ({
100✔
412
  schema: {
413
    isEmail,
414
    isURL,
415
    minLength,
416
    maxLength,
417
    isUUID,
418
    isCUID,
419
    isCUID2,
420
    isULID,
421
    isIP,
422
    isEmoji,
423
    isDatetime,
424
    _def: { checks },
425
  },
426
}) => {
427
  const regexCheck = checks.find(
965✔
428
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
429
      check.kind === "regex",
200✔
430
  );
431
  const datetimeCheck = checks.find(
965✔
432
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
433
      check.kind === "datetime",
205✔
434
  );
435
  const regex = regexCheck
965✔
436
    ? regexCheck.regex
437
    : datetimeCheck
920✔
438
    ? datetimeCheck.offset
10✔
439
      ? new RegExp(
440
          `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`,
441
        )
442
      : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
443
    : undefined;
444
  return {
965✔
445
    type: "string" as const,
446
    ...(isDatetime && { format: "date-time" }),
975✔
447
    ...(isEmail && { format: "email" }),
980✔
448
    ...(isURL && { format: "url" }),
975✔
449
    ...(isUUID && { format: "uuid" }),
975✔
450
    ...(isCUID && { format: "cuid" }),
975✔
451
    ...(isCUID2 && { format: "cuid2" }),
970✔
452
    ...(isULID && { format: "ulid" }),
970✔
453
    ...(isIP && { format: "ip" }),
970✔
454
    ...(isEmoji && { format: "emoji" }),
970✔
455
    ...(minLength !== null && { minLength }),
1,015✔
456
    ...(maxLength !== null && { maxLength }),
985✔
457
    ...(regex && { pattern: `/${regex.source}/${regex.flags}` }),
1,020✔
458
  };
459
};
460

461
/** @todo support exclusive min/max as numbers in case of OpenAPI v3.1.x */
462
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
100✔
463
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
215✔
464
    | Extract<z.ZodNumberCheck, { kind: "min" }>
465
    | undefined;
466
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
215✔
467
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
235✔
468
    | Extract<z.ZodNumberCheck, { kind: "max" }>
469
    | undefined;
470
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
215✔
471
  return {
215✔
472
    type: schema.isInt ? ("integer" as const) : ("number" as const),
215✔
473
    format: schema.isInt ? ("int64" as const) : ("double" as const),
215✔
474
    minimum:
475
      schema.minValue === null
215✔
476
        ? schema.isInt
120✔
477
          ? Number.MIN_SAFE_INTEGER
478
          : Number.MIN_VALUE
479
        : schema.minValue,
480
    exclusiveMinimum: !isMinInclusive,
481
    maximum:
482
      schema.maxValue === null
215✔
483
        ? schema.isInt
180✔
484
          ? Number.MAX_SAFE_INTEGER
485
          : Number.MAX_VALUE
486
        : schema.maxValue,
487
    exclusiveMaximum: !isMaxInclusive,
488
  };
489
};
490

491
export const depictObjectProperties = ({
100✔
492
  schema: { shape },
493
  next,
494
}: Parameters<Depicter<z.AnyZodObject>>[0]) => {
495
  return Object.keys(shape).reduce(
1,435✔
496
    (carry, key) => ({
2,240✔
497
      ...carry,
498
      [key]: next({ schema: shape[key] }),
499
    }),
500
    {} as Record<string, SchemaObject | ReferenceObject>,
501
  );
502
};
503

504
const makeSample = (depicted: SchemaObject) => {
100✔
505
  const type = (
506
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
35!
507
  ) as keyof typeof samples;
508
  return samples?.[type];
35✔
509
};
510

511
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
100✔
512
  schema,
513
  isResponse,
514
  next,
515
}) => {
516
  const input = next({ schema: schema.innerType() });
165✔
517
  const { effect } = schema._def;
165✔
518
  if (isResponse && effect.type === "transform" && isSchemaObject(input)) {
165✔
519
    const outputType = tryToTransform({ effect, sample: makeSample(input) });
35✔
520
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
35✔
521
      return { type: outputType as "number" | "string" | "boolean" };
25✔
522
    } else {
523
      return next({ schema: z.any() });
10✔
524
    }
525
  }
526
  if (!isResponse && effect.type === "preprocess" && isSchemaObject(input)) {
130✔
527
    const { type: inputType, ...rest } = input;
15✔
528
    return {
15✔
529
      ...rest,
530
      format: `${rest.format || inputType} (preprocessed)`,
25✔
531
    };
532
  }
533
  return input;
115✔
534
};
535

536
export const depictPipeline: Depicter<
10✔
537
  z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>
538
> = ({ schema, isResponse, next }) =>
100✔
539
  next({ schema: schema._def[isResponse ? "out" : "in"] });
10✔
540

541
export const depictBranded: Depicter<
5✔
542
  z.ZodBranded<z.ZodTypeAny, string | number | symbol>
543
> = ({ schema, next }) => next({ schema: schema.unwrap() });
100✔
544

545
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
100✔
546
  next,
547
  schema: lazy,
548
  serializer: serialize,
549
  getRef,
550
  makeRef,
551
}): ReferenceObject => {
552
  const hash = serialize(lazy.schema);
60✔
553
  return (
60✔
554
    getRef(hash) ||
90✔
555
    (() => {
556
      makeRef(hash, {}); // make empty ref first
30✔
557
      return makeRef(hash, next({ schema: lazy.schema })); // update
30✔
558
    })()
559
  );
560
};
561

562
export const depictExamples = (
100✔
563
  schema: z.ZodTypeAny,
564
  isResponse: boolean,
565
  omitProps: string[] = [],
580✔
566
): MediaExamples => {
567
  const examples = getExamples({
710✔
568
    schema,
569
    variant: isResponse ? "parsed" : "original",
710✔
570
    validate: true,
571
  });
572
  if (examples.length === 0) {
710✔
573
    return {};
370✔
574
  }
575
  return {
340✔
576
    examples: examples.reduce<ExamplesObject>(
577
      (carry, example, index) => ({
355✔
578
        ...carry,
579
        [`example${index + 1}`]: <ExampleObject>{
580
          value:
581
            typeof example === "object" && !Array.isArray(example)
1,055✔
582
              ? omit(omitProps, example)
583
              : example,
584
        },
585
      }),
586
      {},
587
    ),
588
  };
589
};
590

591
export const depictParamExamples = (
100✔
592
  schema: z.ZodTypeAny,
593
  isResponse: boolean,
594
  param: string,
595
): MediaExamples => {
596
  const examples = getExamples({
190✔
597
    schema,
598
    variant: isResponse ? "parsed" : "original",
190✔
599
    validate: true,
600
  });
601
  if (examples.length === 0) {
190✔
602
    return {};
165✔
603
  }
604
  return {
25✔
605
    examples: examples.reduce<ExamplesObject>(
606
      (carry, example, index) =>
607
        param in example
35!
608
          ? {
609
              ...carry,
610
              [`example${index + 1}`]: <ExampleObject>{
611
                value: example[param],
612
              },
613
            }
614
          : carry,
615
      {},
616
    ),
617
  };
618
};
619

620
export const extractObjectSchema = (
100✔
621
  subject: IOSchema,
622
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">,
623
) => {
624
  if (subject instanceof z.ZodObject) {
510✔
625
    return subject;
425✔
626
  }
627
  let objectSchema: z.AnyZodObject;
628
  if (
85✔
629
    subject instanceof z.ZodUnion ||
155✔
630
    subject instanceof z.ZodDiscriminatedUnion
631
  ) {
632
    objectSchema = Array.from(subject.options.values())
20✔
633
      .map((option) => extractObjectSchema(option, ctx))
40✔
634
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
40✔
635
  } else if (subject instanceof z.ZodEffects) {
65✔
636
    if (hasTopLevelTransformingEffect(subject)) {
15✔
637
      throw new DocumentationError({
5✔
638
        message: `Using transformations on the top level of ${
639
          ctx.isResponse ? "response" : "input"
5!
640
        } schema is not allowed.`,
641
        ...ctx,
642
      });
643
    }
644
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
10✔
645
  } else {
646
    // intersection
647
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
50✔
648
      extractObjectSchema(subject._def.right, ctx),
649
    );
650
  }
651
  return copyMeta(subject, objectSchema);
80✔
652
};
653

654
export const depictRequestParams = ({
315✔
655
  path,
656
  method,
657
  endpoint,
658
  inputSources,
659
  serializer,
660
  getRef,
661
  makeRef,
662
  composition,
663
  clue = "parameter",
315✔
664
}: ReqResDepictHelperCommonProps & {
665
  inputSources: InputSource[];
666
}): ParameterObject[] => {
667
  const schema = endpoint.getSchema("input");
315✔
668
  const shape = extractObjectSchema(schema, {
315✔
669
    path,
670
    method,
671
    isResponse: false,
672
  }).shape;
673
  const pathParams = getRoutePathParams(path);
315✔
674
  const isQueryEnabled = inputSources.includes("query");
315✔
675
  const areParamsEnabled = inputSources.includes("params");
315✔
676
  const areHeadersEnabled = inputSources.includes("headers");
315✔
677
  const isPathParam = (name: string) =>
315✔
678
    areParamsEnabled && pathParams.includes(name);
525✔
679
  const isHeaderParam = (name: string) =>
315✔
680
    areHeadersEnabled && isCustomHeader(name);
145✔
681
  return Object.keys(shape)
315✔
682
    .filter((name) => isQueryEnabled || isPathParam(name))
505✔
683
    .map((name) => {
684
      const depicted = walkSchema({
180✔
685
        schema: shape[name],
686
        isResponse: false,
687
        rules: depicters,
688
        onEach,
689
        onMissing,
690
        serializer,
691
        getRef,
692
        makeRef,
693
        path,
694
        method,
695
      });
696
      const result =
697
        composition === "components"
180✔
698
          ? makeRef(makeCleanId(path, method, `${clue} ${name}`), depicted)
699
          : depicted;
700
      return {
180✔
701
        name,
702
        in: isPathParam(name)
180✔
703
          ? "path"
704
          : isHeaderParam(name)
145✔
705
          ? "header"
706
          : "query",
707
        required: !shape[name].isOptional(),
708
        description:
709
          (isSchemaObject(depicted) && depicted.description) ||
520✔
710
          `${method.toUpperCase()} ${path} ${clue}`,
711
        schema: result,
712
        ...depictParamExamples(schema, false, name),
713
      };
714
    });
715
};
716

717
export const depicters: HandlingRules<
285✔
718
  SchemaObject | ReferenceObject,
719
  OpenAPIContext
720
> = {
100✔
721
  ZodString: depictString,
722
  ZodNumber: depictNumber,
723
  ZodBigInt: depictBigInt,
724
  ZodBoolean: depictBoolean,
725
  ZodDateIn: depictDateIn,
726
  ZodDateOut: depictDateOut,
727
  ZodNull: depictNull,
728
  ZodArray: depictArray,
729
  ZodTuple: depictTuple,
730
  ZodRecord: depictRecord,
731
  ZodObject: depictObject,
732
  ZodLiteral: depictLiteral,
733
  ZodIntersection: depictIntersection,
734
  ZodUnion: depictUnion,
735
  ZodFile: depictFile,
736
  ZodUpload: depictUpload,
737
  ZodAny: depictAny,
738
  ZodDefault: depictDefault,
739
  ZodEnum: depictEnum,
740
  ZodNativeEnum: depictEnum,
741
  ZodEffects: depictEffect,
742
  ZodOptional: depictOptional,
743
  ZodNullable: depictNullable,
744
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
745
  ZodBranded: depictBranded,
746
  ZodDate: depictDate,
747
  ZodCatch: depictCatch,
748
  ZodPipeline: depictPipeline,
749
  ZodLazy: depictLazy,
750
  ZodReadonly: depictReadonly,
751
};
752

753
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
285✔
754
  schema,
755
  isResponse,
756
  prev,
757
}) => {
758
  if (isReferenceObject(prev)) {
3,805✔
759
    return {};
45✔
760
  }
761
  const { description } = schema;
3,760✔
762
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
3,760✔
763
  const hasTypePropertyInDepiction = prev.type !== undefined;
3,760✔
764
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
3,760✔
765
  const isActuallyNullable =
766
    !shouldAvoidParsing &&
3,760✔
767
    hasTypePropertyInDepiction &&
768
    !isResponseHavingCoercion &&
769
    schema.isNullable();
770
  const examples = shouldAvoidParsing
3,760!
771
    ? []
772
    : getExamples({
773
        schema,
774
        variant: isResponse ? "parsed" : "original",
3,760✔
775
        validate: true,
776
      });
777
  return {
3,760✔
778
    ...(description && { description }),
3,785✔
779
    ...(isActuallyNullable && { nullable: true }),
3,815✔
780
    ...(examples.length > 0 && { example: examples[0] }),
4,160✔
781
  };
782
};
783

784
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({
285✔
785
  schema,
786
  ...ctx
787
}) => {
788
  throw new DocumentationError({
35✔
789
    message: `Zod type ${schema.constructor.name} is unsupported.`,
790
    ...ctx,
791
  });
792
};
793

794
export const excludeParamsFromDepiction = (
100✔
795
  depicted: SchemaObject | ReferenceObject,
796
  pathParams: string[],
797
): SchemaObject | ReferenceObject => {
798
  if (isReferenceObject(depicted)) {
230✔
799
    return depicted;
5✔
800
  }
801
  const properties = depicted.properties
225✔
802
    ? omit(pathParams, depicted.properties)
803
    : undefined;
804
  const example = depicted.example
225✔
805
    ? omit(pathParams, depicted.example)
806
    : undefined;
807
  const required = depicted.required
225✔
808
    ? depicted.required.filter((name) => !pathParams.includes(name))
325✔
809
    : undefined;
810
  const allOf = depicted.allOf
225✔
811
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
812
        excludeParamsFromDepiction(entry, pathParams),
60✔
813
      )
814
    : undefined;
815
  const oneOf = depicted.oneOf
225✔
816
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
817
        excludeParamsFromDepiction(entry, pathParams),
30✔
818
      )
819
    : undefined;
820

821
  return omit(
225✔
822
    Object.entries({ properties, required, example, allOf, oneOf })
823
      .filter(([{}, value]) => value === undefined)
1,125✔
824
      .map(([key]) => key),
695✔
825
    {
826
      ...depicted,
827
      properties,
828
      required,
829
      example,
830
      allOf,
831
      oneOf,
832
    },
833
  );
834
};
835

836
export const excludeExampleFromDepiction = (
100✔
837
  depicted: SchemaObject | ReferenceObject,
838
): SchemaObject | ReferenceObject =>
839
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
705!
840

841
export const depictResponse = ({
580✔
842
  method,
843
  path,
844
  endpoint,
845
  isPositive,
846
  serializer,
847
  getRef,
848
  makeRef,
849
  composition,
850
  clue = "response",
×
851
}: ReqResDepictHelperCommonProps & {
852
  isPositive: boolean;
853
}): ResponseObject => {
854
  const schema = endpoint.getSchema(isPositive ? "positive" : "negative");
580✔
855
  const mimeTypes = endpoint.getMimeTypes(isPositive ? "positive" : "negative");
580✔
856
  const depictedSchema = excludeExampleFromDepiction(
580✔
857
    walkSchema({
858
      schema,
859
      isResponse: true,
860
      rules: depicters,
861
      onEach,
862
      onMissing,
863
      serializer,
864
      getRef,
865
      makeRef,
866
      path,
867
      method,
868
    }),
869
  );
870
  const examples = depictExamples(schema, true);
580✔
871
  const result =
872
    composition === "components"
580✔
873
      ? makeRef(makeCleanId(path, method, clue), depictedSchema)
874
      : depictedSchema;
875

876
  return {
580✔
877
    description: `${method.toUpperCase()} ${path} ${clue}`,
878
    content: mimeTypes.reduce(
879
      (carry, mimeType) => ({
585✔
880
        ...carry,
881
        [mimeType]: { schema: result, ...examples },
882
      }),
883
      {} as ContentObject,
884
    ),
885
  };
886
};
887

888
type SecurityHelper<K extends Security["type"]> = (
889
  security: Security & { type: K },
890
) => SecuritySchemeObject;
891

892
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
100✔
893
  type: "http",
894
  scheme: "basic",
895
});
896
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
100✔
897
  format: bearerFormat,
898
}) => ({
20✔
899
  type: "http",
900
  scheme: "bearer",
901
  ...(bearerFormat && { bearerFormat }),
25✔
902
});
903
// @todo add description on actual input placement
904
const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
100✔
905
  type: "apiKey",
906
  in: "query", // body is not supported yet, https://swagger.io/docs/specification/authentication/api-keys/
907
  name,
908
});
909
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
100✔
910
  type: "apiKey",
911
  in: "header",
912
  name,
913
});
914
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
100✔
915
  type: "apiKey",
916
  in: "cookie",
917
  name,
918
});
919
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
100✔
920
  url: openIdConnectUrl,
921
}) => ({
5✔
922
  type: "openIdConnect",
923
  openIdConnectUrl,
924
});
925
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
100✔
926
  type: "oauth2",
927
  flows: (
928
    Object.keys(flows) as (keyof typeof flows)[]
929
  ).reduce<OAuthFlowsObject>((acc, key) => {
930
    const flow = flows[key];
35✔
931
    if (!flow) {
35✔
932
      return acc;
10✔
933
    }
934
    const { scopes = {}, ...rest } = flow;
25!
935
    return { ...acc, [key]: { ...rest, scopes } };
25✔
936
  }, {}),
937
});
938

939
export const depictSecurity = (
280✔
940
  container: LogicalContainer<Security>,
941
): LogicalContainer<SecuritySchemeObject> => {
942
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
280✔
943
    basic: depictBasicSecurity,
944
    bearer: depictBearerSecurity,
945
    input: depictInputSecurity,
946
    header: depictHeaderSecurity,
947
    cookie: depictCookieSecurity,
948
    openid: depictOpenIdSecurity,
949
    oauth2: depictOAuth2Security,
950
  };
951
  return mapLogicalContainer(container, (security) =>
280✔
952
    (methods[security.type] as SecurityHelper<typeof security.type>)(security),
90✔
953
  );
954
};
955

956
export const depictSecurityRefs = (
285✔
957
  container: LogicalContainer<{ name: string; scopes: string[] }>,
958
): SecurityRequirementObject[] => {
959
  if (typeof container === "object") {
550✔
960
    if ("or" in container) {
550✔
961
      return container.or.map((entry) =>
285✔
962
        ("and" in entry
90✔
963
          ? entry.and
964
          : [entry]
965
        ).reduce<SecurityRequirementObject>(
966
          (agg, { name, scopes }) => ({
130✔
967
            ...agg,
968
            [name]: scopes,
969
          }),
970
          {},
971
        ),
972
      );
973
    }
974
    if ("and" in container) {
265✔
975
      return depictSecurityRefs(andToOr(container));
260✔
976
    }
977
  }
978
  return depictSecurityRefs({ or: [container] });
5✔
979
};
980

981
export const depictRequest = ({
155✔
982
  method,
983
  path,
984
  endpoint,
985
  serializer,
986
  getRef,
987
  makeRef,
988
  composition,
989
  clue = "request body",
155✔
990
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
991
  const pathParams = getRoutePathParams(path);
155✔
992
  const bodyDepiction = excludeExampleFromDepiction(
155✔
993
    excludeParamsFromDepiction(
994
      walkSchema({
995
        schema: endpoint.getSchema("input"),
996
        isResponse: false,
997
        rules: depicters,
998
        onEach,
999
        onMissing,
1000
        serializer,
1001
        getRef,
1002
        makeRef,
1003
        path,
1004
        method,
1005
      }),
1006
      pathParams,
1007
    ),
1008
  );
1009
  const bodyExamples = depictExamples(
120✔
1010
    endpoint.getSchema("input"),
1011
    false,
1012
    pathParams,
1013
  );
1014
  const result =
1015
    composition === "components"
120✔
1016
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
1017
      : bodyDepiction;
1018

1019
  return {
120✔
1020
    description: `${method.toUpperCase()} ${path} ${clue}`,
1021
    content: endpoint.getMimeTypes("input").reduce(
1022
      (carry, mimeType) => ({
120✔
1023
        ...carry,
1024
        [mimeType]: { schema: result, ...bodyExamples },
1025
      }),
1026
      {} as ContentObject,
1027
    ),
1028
  };
1029
};
1030

1031
export const depictTags = <TAG extends string>(
100✔
1032
  tags: TagsConfig<TAG>,
1033
): TagObject[] =>
1034
  (Object.keys(tags) as TAG[]).map((tag) => {
20✔
1035
    const def = tags[tag];
40✔
1036
    return {
40✔
1037
      name: tag,
1038
      description: typeof def === "string" ? def : def.description,
40✔
1039
      ...(typeof def === "object" &&
55✔
1040
        def.url && { externalDocs: { url: def.url } }),
1041
    };
1042
  });
1043

1044
export const ensureShortDescription = (description: string) => {
100✔
1045
  if (description.length <= shortDescriptionLimit) {
100✔
1046
    return description;
85✔
1047
  }
1048
  return description.slice(0, shortDescriptionLimit - 1) + "…";
15✔
1049
};
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