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

RobinTail / express-zod-api / 6286196427

23 Sep 2023 09:58PM CUT coverage: 100.0%. Remained the same
6286196427

Pull #1165

github

web-flow
Merge 820637495 into e135f0a61
Pull Request #1165: Bump @typescript-eslint/eslint-plugin from 6.7.0 to 6.7.2

548 of 577 branches covered (0.0%)

1175 of 1175 relevant lines covered (100.0%)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

160
export const depictDiscriminatedUnion: Depicter<
4✔
161
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
162
> = ({ schema: { options, discriminator }, next }) => {
84✔
163
  return {
12✔
164
    discriminator: { propertyName: discriminator },
165
    oneOf: Array.from(options.values()).map((option) =>
166
      next({ schema: option }),
24✔
167
    ),
168
  };
169
};
170

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

182
export const depictOptional: Depicter<z.ZodOptional<any>> = ({
84✔
183
  schema,
184
  next,
185
}) => next({ schema: schema.unwrap() });
44✔
186

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

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

200
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
84✔
201
  schema,
202
}) => ({
16✔
203
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
204
  enum: Object.values(schema.enum),
205
});
206

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

738
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
228✔
739
  schema,
740
  isResponse,
741
  prev,
742
}) => {
743
  if (isReferenceObject(prev)) {
2,880✔
744
    return {};
36✔
745
  }
746
  const { description } = schema;
2,844✔
747
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
2,844✔
748
  const hasTypePropertyInDepiction = prev.type !== undefined;
2,844✔
749
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
2,844✔
750
  const isActuallyNullable =
751
    !shouldAvoidParsing &&
2,844✔
752
    hasTypePropertyInDepiction &&
753
    !isResponseHavingCoercion &&
754
    schema.isNullable();
755
  const examples = shouldAvoidParsing
2,844!
756
    ? []
757
    : getExamples({
758
        schema,
759
        variant: isResponse ? "parsed" : "original",
2,844✔
760
        validate: true,
761
      });
762
  return {
2,844✔
763
    ...(description && { description }),
2,864✔
764
    ...(isActuallyNullable && { nullable: true }),
2,888✔
765
    ...(examples.length > 0 && { example: examples[0] }),
3,144✔
766
  };
767
};
768

769
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({
228✔
770
  schema,
771
  ...ctx
772
}) => {
773
  throw new DocumentationError({
28✔
774
    message: `Zod type ${schema.constructor.name} is unsupported.`,
775
    ...ctx,
776
  });
777
};
778

779
export const excludeParamsFromDepiction = (
84✔
780
  depicted: SchemaObject | ReferenceObject,
781
  pathParams: string[],
782
): SchemaObject | ReferenceObject => {
783
  if (isReferenceObject(depicted)) {
180✔
784
    return depicted;
4✔
785
  }
786
  const properties = depicted.properties
176✔
787
    ? omit(pathParams, depicted.properties)
788
    : undefined;
789
  const example = depicted.example
176✔
790
    ? omit(pathParams, depicted.example)
791
    : undefined;
792
  const required = depicted.required
176✔
793
    ? depicted.required.filter((name) => !pathParams.includes(name))
260✔
794
    : undefined;
795
  const allOf = depicted.allOf
176✔
796
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
797
        excludeParamsFromDepiction(entry, pathParams),
48✔
798
      )
799
    : undefined;
800
  const oneOf = depicted.oneOf
176✔
801
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
802
        excludeParamsFromDepiction(entry, pathParams),
24✔
803
      )
804
    : undefined;
805

806
  return omit(
176✔
807
    Object.entries({ properties, required, example, allOf, oneOf })
808
      .filter(([{}, value]) => value === undefined)
880✔
809
      .map(([key]) => key),
540✔
810
    {
811
      ...depicted,
812
      properties,
813
      required,
814
      example,
815
      allOf,
816
      oneOf,
817
    },
818
  );
819
};
820

821
export const excludeExampleFromDepiction = (
84✔
822
  depicted: SchemaObject | ReferenceObject,
823
): SchemaObject | ReferenceObject =>
824
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
520!
825

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

861
  return {
424✔
862
    description: `${method.toUpperCase()} ${path} ${clue}`,
863
    content: mimeTypes.reduce(
864
      (carry, mimeType) => ({
428✔
865
        ...carry,
866
        [mimeType]: { schema: result, ...examples },
867
      }),
868
      {} as ContentObject,
869
    ),
870
  };
871
};
872

873
type SecurityHelper<K extends Security["type"]> = (
874
  security: Security & { type: K },
875
) => SecuritySchemeObject;
876

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

924
export const depictSecurity = (
204✔
925
  container: LogicalContainer<Security>,
926
): LogicalContainer<SecuritySchemeObject> => {
927
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
204✔
928
    basic: depictBasicSecurity,
929
    bearer: depictBearerSecurity,
930
    input: depictInputSecurity,
931
    header: depictHeaderSecurity,
932
    cookie: depictCookieSecurity,
933
    openid: depictOpenIdSecurity,
934
    oauth2: depictOAuth2Security,
935
  };
936
  return mapLogicalContainer(container, (security) =>
204✔
937
    (methods[security.type] as SecurityHelper<typeof security.type>)(security),
72✔
938
  );
939
};
940

941
export const depictSecurityRefs = (
208✔
942
  container: LogicalContainer<{ name: string; scopes: string[] }>,
943
): SecurityRequirementObject[] => {
944
  if (typeof container === "object") {
400✔
945
    if ("or" in container) {
400✔
946
      return container.or.map((entry) =>
208✔
947
        ("and" in entry
72✔
948
          ? entry.and
949
          : [entry]
950
        ).reduce<SecurityRequirementObject>(
951
          (agg, { name, scopes }) => ({
104✔
952
            ...agg,
953
            [name]: scopes,
954
          }),
955
          {},
956
        ),
957
      );
958
    }
959
    if ("and" in container) {
192✔
960
      return depictSecurityRefs(andToOr(container));
188✔
961
    }
962
  }
963
  return depictSecurityRefs({ or: [container] });
4✔
964
};
965

966
export const depictRequest = ({
120✔
967
  method,
968
  path,
969
  endpoint,
970
  serializer,
971
  getRef,
972
  makeRef,
973
  composition,
974
  clue = "request body",
120✔
975
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
976
  const pathParams = getRoutePathParams(path);
120✔
977
  const bodyDepiction = excludeExampleFromDepiction(
120✔
978
    excludeParamsFromDepiction(
979
      walkSchema({
980
        schema: endpoint.getSchema("input"),
981
        isResponse: false,
982
        rules: depicters,
983
        onEach,
984
        onMissing,
985
        serializer,
986
        getRef,
987
        makeRef,
988
        path,
989
        method,
990
      }),
991
      pathParams,
992
    ),
993
  );
994
  const bodyExamples = depictExamples(
92✔
995
    endpoint.getSchema("input"),
996
    false,
997
    pathParams,
998
  );
999
  const result =
1000
    composition === "components"
92✔
1001
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
1002
      : bodyDepiction;
1003

1004
  return {
92✔
1005
    description: `${method.toUpperCase()} ${path} ${clue}`,
1006
    content: endpoint.getMimeTypes("input").reduce(
1007
      (carry, mimeType) => ({
92✔
1008
        ...carry,
1009
        [mimeType]: { schema: result, ...bodyExamples },
1010
      }),
1011
      {} as ContentObject,
1012
    ),
1013
  };
1014
};
1015

1016
export const depictTags = <TAG extends string>(
84✔
1017
  tags: TagsConfig<TAG>,
1018
): TagObject[] =>
1019
  (Object.keys(tags) as TAG[]).map((tag) => {
16✔
1020
    const def = tags[tag];
32✔
1021
    return {
32✔
1022
      name: tag,
1023
      description: typeof def === "string" ? def : def.description,
32✔
1024
      ...(typeof def === "object" &&
44✔
1025
        def.url && { externalDocs: { url: def.url } }),
1026
    };
1027
  });
1028

1029
export const ensureShortDescription = (description: string) => {
84✔
1030
  if (description.length <= shortDescriptionLimit) {
64✔
1031
    return description;
52✔
1032
  }
1033
  return description.slice(0, shortDescriptionLimit - 1) + "…";
12✔
1034
};
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