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

RobinTail / express-zod-api / 6950624855

21 Nov 2023 11:11PM CUT coverage: 100.0%. Remained the same
6950624855

Pull #1329

github

web-flow
Merge 2786d93e9 into f51075c3a
Pull Request #1329: Bump @typescript-eslint/eslint-plugin from 6.11.0 to 6.12.0

570 of 594 branches covered (0.0%)

1214 of 1214 relevant lines covered (100.0%)

359.35 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
  hasRaw,
26
  hasTopLevelTransformingEffect,
27
  isCustomHeader,
28
  makeCleanId,
29
  tryToTransform,
30
} from "./common-helpers";
100✔
31
import { InputSource, TagsConfig } from "./config-type";
32
import { ZodDateIn, isoDateRegex } from "./date-in-schema";
100✔
33
import { ZodDateOut } from "./date-out-schema";
34
import { AbstractEndpoint } from "./endpoint";
35
import { DocumentationError } from "./errors";
100✔
36
import { ZodFile } from "./file-schema";
100✔
37
import { IOSchema } from "./io-schema";
38
import {
39
  LogicalContainer,
40
  andToOr,
41
  mapLogicalContainer,
42
} from "./logical-container";
100✔
43
import { copyMeta } from "./metadata";
100✔
44
import { Method } from "./method";
45
import {
46
  HandlingRules,
47
  HandlingVariant,
48
  SchemaHandler,
49
  walkSchema,
50
} from "./schema-walker";
100✔
51
import { Security } from "./security";
52
import { ZodUpload } from "./upload-schema";
53

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

193
export const depictNullable: Depicter<z.ZodNullable<z.ZodTypeAny>> = ({
100✔
194
  schema,
195
  next,
196
}) => ({
30✔
197
  nullable: true,
198
  ...next({ schema: schema.unwrap() }),
199
});
200

201
export const depictEnum: Depicter<
10✔
202
  z.ZodEnum<[string, ...string[]]> | z.ZodNativeEnum<any> // keeping "any" for ZodNativeEnum as compatibility fix
203
> = ({ schema }) => ({
100✔
204
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
205
  enum: Object.values(schema.enum),
206
});
207

208
export const depictLiteral: Depicter<z.ZodLiteral<unknown>> = ({
100✔
209
  schema: { value },
210
}) => ({
615✔
211
  type: typeof value as "string" | "number" | "boolean",
212
  enum: [value],
213
});
214

215
export const depictObject: Depicter<z.AnyZodObject> = ({
100✔
216
  schema,
217
  isResponse,
218
  ...rest
219
}) => {
220
  const required = Object.keys(schema.shape).filter((key) => {
1,440✔
221
    const prop = schema.shape[key];
2,240✔
222
    const isOptional =
223
      isResponse && hasCoercion(prop)
2,240✔
224
        ? prop instanceof z.ZodOptional
225
        : prop.isOptional();
226
    return !isOptional;
2,240✔
227
  });
228
  return {
1,440✔
229
    type: "object",
230
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
231
    ...(required.length ? { required } : {}),
1,405✔
232
  };
233
};
234

235
/**
236
 * @see https://swagger.io/docs/specification/data-models/data-types/
237
 * @todo use type:"null" for OpenAPI 3.1
238
 * */
239
export const depictNull: Depicter<z.ZodNull> = () => ({
100✔
240
  type: "string",
241
  nullable: true,
242
  format: "null",
243
});
244

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

263
export const depictDateOut: Depicter<ZodDateOut> = (ctx) => {
100✔
264
  if (!ctx.isResponse) {
25✔
265
    throw new DocumentationError({
5✔
266
      message: "Please use z.dateIn() for input.",
267
      ...ctx,
268
    });
269
  }
270
  return {
20✔
271
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
272
    type: "string",
273
    format: "date-time",
274
    externalDocs: {
275
      url: isoDateDocumentationUrl,
276
    },
277
  };
278
};
279

280
/** @throws DocumentationError */
281
export const depictDate: Depicter<z.ZodDate> = (ctx) => {
100✔
282
  throw new DocumentationError({
10✔
283
    message: `Using z.date() within ${
284
      ctx.isResponse ? "output" : "input"
10✔
285
    } schema is forbidden. Please use z.date${
286
      ctx.isResponse ? "Out" : "In"
10✔
287
    }() instead. Check out the documentation for details.`,
288
    ...ctx,
289
  });
290
};
291

292
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
155✔
293
  type: "boolean",
294
});
295

296
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
100✔
297
  type: "integer",
298
  format: "bigint",
299
});
300

301
const areOptionsLiteral = (
100✔
302
  subject: z.ZodTypeAny[],
303
): subject is z.ZodLiteral<unknown>[] =>
304
  subject.reduce(
10✔
305
    (carry, option) => carry && option instanceof z.ZodLiteral,
20✔
306
    true,
307
  );
308

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

368
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
100✔
369
  schema: { _def: def, element },
370
  next,
371
}) => ({
75✔
372
  type: "array",
373
  items: next({ schema: element }),
374
  ...(def.minLength !== null && { minItems: def.minLength.value }),
100✔
375
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
80✔
376
});
377

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

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

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

483
export const depictObjectProperties = ({
100✔
484
  schema: { shape },
485
  next,
486
}: Parameters<Depicter<z.AnyZodObject>>[0]) => {
487
  return Object.keys(shape).reduce(
1,475✔
488
    (carry, key) => ({
2,300✔
489
      ...carry,
490
      [key]: next({ schema: shape[key] }),
491
    }),
492
    {} as Record<string, SchemaObject | ReferenceObject>,
493
  );
494
};
495

496
const makeSample = (depicted: SchemaObject) => {
100✔
497
  const type = (
498
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
35!
499
  ) as keyof typeof samples;
500
  return samples?.[type];
35✔
501
};
502

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

528
export const depictPipeline: Depicter<
10✔
529
  z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>
530
> = ({ schema, isResponse, next }) =>
100✔
531
  next({ schema: schema._def[isResponse ? "out" : "in"] });
10✔
532

533
export const depictBranded: Depicter<
5✔
534
  z.ZodBranded<z.ZodTypeAny, string | number | symbol>
535
> = ({ schema, next }) => next({ schema: schema.unwrap() });
100✔
536

537
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
100✔
538
  next,
539
  schema: lazy,
540
  serializer: serialize,
541
  getRef,
542
  makeRef,
543
}): ReferenceObject => {
544
  const hash = serialize(lazy.schema);
60✔
545
  return (
60✔
546
    getRef(hash) ||
90✔
547
    (() => {
548
      makeRef(hash, {}); // make empty ref first
30✔
549
      return makeRef(hash, next({ schema: lazy.schema })); // update
30✔
550
    })()
551
  );
552
};
553

554
export const depictExamples = (
100✔
555
  schema: z.ZodTypeAny,
556
  isResponse: boolean,
557
  omitProps: string[] = [],
600✔
558
): MediaExamples => {
559
  const examples = getExamples({
740✔
560
    schema,
561
    variant: isResponse ? "parsed" : "original",
740✔
562
    validate: true,
563
  });
564
  if (examples.length === 0) {
740✔
565
    return {};
390✔
566
  }
567
  return {
350✔
568
    examples: examples.reduce<ExamplesObject>(
569
      (carry, example, index) => ({
365✔
570
        ...carry,
571
        [`example${index + 1}`]: <ExampleObject>{
572
          value:
573
            typeof example === "object" && !Array.isArray(example)
1,085✔
574
              ? omit(omitProps, example)
575
              : example,
576
        },
577
      }),
578
      {},
579
    ),
580
  };
581
};
582

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

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

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

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

745
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
285✔
746
  schema,
747
  isResponse,
748
  prev,
749
}) => {
750
  if (isReferenceObject(prev)) {
3,895✔
751
    return {};
45✔
752
  }
753
  const { description } = schema;
3,850✔
754
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
3,850✔
755
  const hasTypePropertyInDepiction = prev.type !== undefined;
3,850✔
756
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
3,850✔
757
  const isActuallyNullable =
758
    !shouldAvoidParsing &&
3,850✔
759
    hasTypePropertyInDepiction &&
760
    !isResponseHavingCoercion &&
761
    schema.isNullable();
762
  const examples = shouldAvoidParsing
3,850!
763
    ? []
764
    : getExamples({
765
        schema,
766
        variant: isResponse ? "parsed" : "original",
3,850✔
767
        validate: true,
768
      });
769
  return {
3,850✔
770
    ...(description && { description }),
3,875✔
771
    ...(isActuallyNullable && { nullable: true }),
3,905✔
772
    ...(examples.length > 0 && { example: examples[0] }),
4,260✔
773
  };
774
};
775

776
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({
285✔
777
  schema,
778
  ...ctx
779
}) => {
780
  throw new DocumentationError({
35✔
781
    message: `Zod type ${schema.constructor.name} is unsupported.`,
782
    ...ctx,
783
  });
784
};
785

786
export const excludeParamsFromDepiction = (
100✔
787
  depicted: SchemaObject | ReferenceObject,
788
  pathParams: string[],
789
): SchemaObject | ReferenceObject => {
790
  if (isReferenceObject(depicted)) {
240✔
791
    return depicted;
5✔
792
  }
793
  const properties = depicted.properties
235✔
794
    ? omit(pathParams, depicted.properties)
795
    : undefined;
796
  const example = depicted.example
235✔
797
    ? omit(pathParams, depicted.example)
798
    : undefined;
799
  const required = depicted.required
235✔
800
    ? depicted.required.filter((name) => !pathParams.includes(name))
325✔
801
    : undefined;
802
  const allOf = depicted.allOf
235✔
803
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
804
        excludeParamsFromDepiction(entry, pathParams),
60✔
805
      )
806
    : undefined;
807
  const oneOf = depicted.oneOf
235✔
808
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
809
        excludeParamsFromDepiction(entry, pathParams),
30✔
810
      )
811
    : undefined;
812

813
  return omit(
235✔
814
    Object.entries({ properties, required, example, allOf, oneOf })
815
      .filter(([{}, value]) => value === undefined)
1,175✔
816
      .map(([key]) => key),
745✔
817
    {
818
      ...depicted,
819
      properties,
820
      required,
821
      example,
822
      allOf,
823
      oneOf,
824
    },
825
  );
826
};
827

828
export const excludeExampleFromDepiction = (
100✔
829
  depicted: SchemaObject | ReferenceObject,
830
): SchemaObject | ReferenceObject =>
831
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
735!
832

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

868
  return {
600✔
869
    description: `${method.toUpperCase()} ${path} ${clue}`,
870
    content: mimeTypes.reduce(
871
      (carry, mimeType) => ({
605✔
872
        ...carry,
873
        [mimeType]: { schema: result, ...examples },
874
      }),
875
      {} as ContentObject,
876
    ),
877
  };
878
};
879

880
type SecurityHelper<K extends Security["type"]> = (
881
  security: Security & { type: K },
882
) => SecuritySchemeObject;
883

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

931
export const depictSecurity = (
290✔
932
  container: LogicalContainer<Security>,
933
): LogicalContainer<SecuritySchemeObject> => {
934
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
290✔
935
    basic: depictBasicSecurity,
936
    bearer: depictBearerSecurity,
937
    input: depictInputSecurity,
938
    header: depictHeaderSecurity,
939
    cookie: depictCookieSecurity,
940
    openid: depictOpenIdSecurity,
941
    oauth2: depictOAuth2Security,
942
  };
943
  return mapLogicalContainer(container, (security) =>
290✔
944
    (methods[security.type] as SecurityHelper<typeof security.type>)(security),
90✔
945
  );
946
};
947

948
export const depictSecurityRefs = (
295✔
949
  container: LogicalContainer<{ name: string; scopes: string[] }>,
950
): SecurityRequirementObject[] => {
951
  if (typeof container === "object") {
570✔
952
    if ("or" in container) {
570✔
953
      return container.or.map((entry) =>
295✔
954
        ("and" in entry
90✔
955
          ? entry.and
956
          : [entry]
957
        ).reduce<SecurityRequirementObject>(
958
          (agg, { name, scopes }) => ({
130✔
959
            ...agg,
960
            [name]: scopes,
961
          }),
962
          {},
963
        ),
964
      );
965
    }
966
    if ("and" in container) {
275✔
967
      return depictSecurityRefs(andToOr(container));
270✔
968
    }
969
  }
970
  return depictSecurityRefs({ or: [container] });
5✔
971
};
972

973
export const depictRequest = ({
165✔
974
  method,
975
  path,
976
  endpoint,
977
  serializer,
978
  getRef,
979
  makeRef,
980
  composition,
981
  clue = "request body",
165✔
982
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
983
  const pathParams = getRoutePathParams(path);
165✔
984
  const inputSchema = endpoint.getSchema("input");
165✔
985
  const bodyDepiction = excludeExampleFromDepiction(
165✔
986
    excludeParamsFromDepiction(
987
      walkSchema({
988
        schema: hasRaw(inputSchema) ? ZodFile.create().buffer() : inputSchema,
165✔
989
        isResponse: false,
990
        rules: depicters,
991
        onEach,
992
        onMissing,
993
        serializer,
994
        getRef,
995
        makeRef,
996
        path,
997
        method,
998
      }),
999
      pathParams,
1000
    ),
1001
  );
1002
  const bodyExamples = depictExamples(
130✔
1003
    endpoint.getSchema("input"),
1004
    false,
1005
    pathParams,
1006
  );
1007
  const result =
1008
    composition === "components"
130✔
1009
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
1010
      : bodyDepiction;
1011

1012
  return {
130✔
1013
    description: `${method.toUpperCase()} ${path} ${clue}`,
1014
    content: endpoint.getMimeTypes("input").reduce(
1015
      (carry, mimeType) => ({
130✔
1016
        ...carry,
1017
        [mimeType]: { schema: result, ...bodyExamples },
1018
      }),
1019
      {} as ContentObject,
1020
    ),
1021
  };
1022
};
1023

1024
export const depictTags = <TAG extends string>(
100✔
1025
  tags: TagsConfig<TAG>,
1026
): TagObject[] =>
1027
  (Object.keys(tags) as TAG[]).map((tag) => {
20✔
1028
    const def = tags[tag];
40✔
1029
    return {
40✔
1030
      name: tag,
1031
      description: typeof def === "string" ? def : def.description,
40✔
1032
      ...(typeof def === "object" &&
55✔
1033
        def.url && { externalDocs: { url: def.url } }),
1034
    };
1035
  });
1036

1037
export const ensureShortDescription = (description: string) => {
100✔
1038
  if (description.length <= shortDescriptionLimit) {
100✔
1039
    return description;
85✔
1040
  }
1041
  return description.slice(0, shortDescriptionLimit - 1) + "…";
15✔
1042
};
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