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

RobinTail / express-zod-api / 6460223901

09 Oct 2023 06:10PM CUT coverage: 100.0%. Remained the same
6460223901

Pull #1222

github

web-flow
Merge 8590d8087 into 3ae174ac0
Pull Request #1222: Bump @typescript-eslint/parser from 6.7.4 to 6.7.5

561 of 591 branches covered (0.0%)

1194 of 1194 relevant lines covered (100.0%)

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

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

56
export interface OpenAPIContext {
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
  endpoint: AbstractEndpoint;
79
  composition: "inline" | "components";
80
  clue?: string;
81
}
82

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

87
const samples: Record<
88
  Exclude<NonNullable<SchemaObjectType>, Array<any>>,
89
  any
90
> = {
105✔
91
  integer: 0,
92
  number: 0,
93
  string: "",
94
  boolean: false,
95
  object: {},
96
  null: null,
97
  array: [],
98
};
99

100
/* eslint-disable @typescript-eslint/no-use-before-define */
101

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

105
export const depictDefault: Depicter<z.ZodDefault<z.ZodTypeAny>> = ({
105✔
106
  schema: {
107
    _def: { innerType, defaultValue },
108
  },
109
  next,
110
}) => ({
15✔
111
  ...next({ schema: innerType }),
112
  default: defaultValue(),
113
});
114

115
export const depictCatch: Depicter<z.ZodCatch<z.ZodTypeAny>> = ({
105✔
116
  schema: {
117
    _def: { innerType },
118
  },
119
  next,
120
}) => next({ schema: innerType });
5✔
121

122
export const depictAny: Depicter<z.ZodAny> = () => ({
105✔
123
  format: "any",
124
});
125

126
export const depictUpload: Depicter<ZodUpload> = (ctx) => {
105✔
127
  if (ctx.isResponse) {
20✔
128
    throw new DocumentationError({
5✔
129
      message: "Please use z.upload() only for input.",
130
      ...ctx,
131
    });
132
  }
133
  return {
15✔
134
    type: "string",
135
    format: "binary",
136
  };
137
};
138

139
export const depictFile: Depicter<ZodFile> = ({
105✔
140
  schema: { isBinary, isBase64 },
141
  ...ctx
142
}) => {
143
  if (!ctx.isResponse) {
30✔
144
    throw new DocumentationError({
5✔
145
      message: "Please use z.file() only within ResultHandler.",
146
      ...ctx,
147
    });
148
  }
149
  return {
25✔
150
    type: "string",
151
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
35✔
152
  };
153
};
154

155
export const depictUnion: Depicter<
5✔
156
  z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
157
> = ({ schema: { options }, next }) => ({
105✔
158
  oneOf: options.map((option) => next({ schema: option })),
90✔
159
});
160

161
export const depictDiscriminatedUnion: Depicter<
5✔
162
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
163
> = ({ schema: { options, discriminator }, next }) => {
105✔
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
> = ({
105✔
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<any>> = ({
105✔
184
  schema,
185
  next,
186
}) => next({ schema: schema.unwrap() });
55✔
187

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

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

201
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
105✔
202
  schema,
203
}) => ({
20✔
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<any>> = ({
105✔
209
  schema: { value },
210
}) => ({
595✔
211
  type: typeof value as "string" | "number" | "boolean",
212
  enum: [value],
213
});
214

215
export const depictObject: Depicter<z.AnyZodObject> = ({
105✔
216
  schema,
217
  isResponse,
218
  ...rest
219
}) => {
220
  const required = Object.keys(schema.shape).filter((key) => {
1,400✔
221
    const prop = schema.shape[key];
2,180✔
222
    const isOptional =
223
      isResponse && hasCoercion(prop)
2,180✔
224
        ? prop instanceof z.ZodOptional
225
        : prop.isOptional();
226
    return !isOptional;
2,180✔
227
  });
228
  return {
1,400✔
229
    type: "object",
230
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
231
    ...(required.length ? { required } : {}),
1,365✔
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> = () => ({
105✔
240
  type: "string",
241
  nullable: true,
242
  format: "null",
243
});
244

245
export const depictDateIn: Depicter<ZodDateIn> = (ctx) => {
105✔
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) => {
105✔
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) => {
105✔
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> = () => ({
105✔
297
  type: "integer",
298
  format: "bigint",
299
});
300

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

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

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

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

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

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

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

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

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

533
export const depictBranded: Depicter<z.ZodBranded<z.ZodTypeAny, any>> = ({
105✔
534
  schema,
535
  next,
536
}) => next({ schema: schema.unwrap() });
5✔
537

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

974
export const depictRequest = ({
155✔
975
  method,
976
  path,
977
  endpoint,
978
  serializer,
979
  getRef,
980
  makeRef,
981
  composition,
982
  clue = "request body",
155✔
983
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
984
  const pathParams = getRoutePathParams(path);
155✔
985
  const bodyDepiction = excludeExampleFromDepiction(
155✔
986
    excludeParamsFromDepiction(
987
      walkSchema({
988
        schema: endpoint.getSchema("input"),
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(
120✔
1003
    endpoint.getSchema("input"),
1004
    false,
1005
    pathParams,
1006
  );
1007
  const result =
1008
    composition === "components"
120✔
1009
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
1010
      : bodyDepiction;
1011

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

1024
export const depictTags = <TAG extends string>(
105✔
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) => {
105✔
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