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

RobinTail / express-zod-api / 7246160078

18 Dec 2023 09:33AM CUT coverage: 100.0%. Remained the same
7246160078

push

github

web-flow
Bump @types/node from 20.10.4 to 20.10.5 (#1389)

Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 20.10.4 to 20.10.5.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=20.10.4&new-version=20.10.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade t... (continued)

675 of 703 branches covered (0.0%)

1079 of 1079 relevant lines covered (100.0%)

433.08 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

149
export const depictFile: Depicter<ZodFile> = ({
120✔
150
  schema: { isBinary, isBase64, isBuffer },
151
}) => ({
42✔
152
  type: "string",
153
  format: isBuffer || isBinary ? "binary" : isBase64 ? "byte" : "file",
114✔
154
});
155

156
export const depictUnion: Depicter<z.ZodUnion<z.ZodUnionOptions>> = ({
120✔
157
  schema: { options },
158
  next,
159
}) => ({
54✔
160
  oneOf: options.map((option) => next({ schema: option })),
108✔
161
});
162

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

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

185
export const depictOptional: Depicter<z.ZodOptional<z.ZodTypeAny>> = ({
120✔
186
  schema,
187
  next,
188
}) => next({ schema: schema.unwrap() });
66✔
189

190
export const depictReadonly: Depicter<z.ZodReadonly<z.ZodTypeAny>> = ({
120✔
191
  schema,
192
  next,
193
}) => next({ schema: schema._def.innerType });
12✔
194

195
export const depictNullable: Depicter<z.ZodNullable<z.ZodTypeAny>> = ({
120✔
196
  schema,
197
  next,
198
}) => ({
36✔
199
  nullable: true,
200
  ...next({ schema: schema.unwrap() }),
201
});
202

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

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

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

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

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

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

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

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

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

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

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

373
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
120✔
374
  schema: { _def: def, element },
375
  next,
376
}) => ({
90✔
377
  type: "array",
378
  items: next({ schema: element }),
379
  ...(def.minLength !== null && { minItems: def.minLength.value }),
120✔
380
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
96✔
381
});
382

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

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

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

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

501
const makeSample = (depicted: SchemaObject) => {
120✔
502
  const type = (
503
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
42!
504
  ) as keyof typeof samples;
505
  return samples?.[type];
42✔
506
};
507

508
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
120✔
509
  schema,
510
  isResponse,
511
  next,
512
}) => {
513
  const input = next({ schema: schema.innerType() });
198✔
514
  const { effect } = schema._def;
198✔
515
  if (isResponse && effect.type === "transform" && isSchemaObject(input)) {
198✔
516
    const outputType = tryToTransform({ effect, sample: makeSample(input) });
42✔
517
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
42✔
518
      return { type: outputType as "number" | "string" | "boolean" };
30✔
519
    } else {
520
      return next({ schema: z.any() });
12✔
521
    }
522
  }
523
  if (!isResponse && effect.type === "preprocess" && isSchemaObject(input)) {
156✔
524
    const { type: inputType, ...rest } = input;
18✔
525
    return {
18✔
526
      ...rest,
527
      format: `${rest.format || inputType} (preprocessed)`,
30✔
528
    };
529
  }
530
  return input;
138✔
531
};
532

533
export const depictPipeline: Depicter<
534
  z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>
535
> = ({ schema, isResponse, next }) =>
120✔
536
  next({ schema: schema._def[isResponse ? "out" : "in"] });
12✔
537

538
export const depictBranded: Depicter<
539
  z.ZodBranded<z.ZodTypeAny, string | number | symbol>
540
> = ({ schema, next }) => next({ schema: schema.unwrap() });
120✔
541

542
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
120✔
543
  next,
544
  schema: lazy,
545
  serializer: serialize,
546
  getRef,
547
  makeRef,
548
}): ReferenceObject => {
549
  const hash = serialize(lazy.schema);
72✔
550
  return (
72✔
551
    getRef(hash) ||
108✔
552
    (() => {
553
      makeRef(hash, {}); // make empty ref first
36✔
554
      return makeRef(hash, next({ schema: lazy.schema })); // update
36✔
555
    })()
556
  );
557
};
558

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

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

617
export const extractObjectSchema = (
120✔
618
  subject: IOSchema,
619
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">,
620
) => {
621
  if (subject instanceof z.ZodObject) {
624✔
622
    return subject;
522✔
623
  }
624
  let objectSchema: z.AnyZodObject;
625
  if (
102✔
626
    subject instanceof z.ZodUnion ||
186✔
627
    subject instanceof z.ZodDiscriminatedUnion
628
  ) {
629
    objectSchema = Array.from(subject.options.values())
24✔
630
      .map((option) => extractObjectSchema(option, ctx))
48✔
631
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
48✔
632
  } else if (subject instanceof z.ZodEffects) {
78✔
633
    assert(
18✔
634
      !hasTopLevelTransformingEffect(subject),
635
      new DocumentationError({
636
        message: `Using transformations on the top level of ${
637
          ctx.isResponse ? "response" : "input"
18!
638
        } schema is not allowed.`,
639
        ...ctx,
640
      }),
641
    );
642
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
12✔
643
  } else {
644
    // intersection
645
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
60✔
646
      extractObjectSchema(subject._def.right, ctx),
647
    );
648
  }
649
  return copyMeta(subject, objectSchema);
96✔
650
};
651

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

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

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

782
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({ schema, ...ctx }) =>
120✔
783
  assert.fail(
42✔
784
    new DocumentationError({
785
      message: `Zod type ${schema.constructor.name} is unsupported.`,
786
      ...ctx,
787
    }),
788
  );
789

790
export const excludeParamsFromDepiction = (
120✔
791
  depicted: SchemaObject | ReferenceObject,
792
  pathParams: string[],
793
): SchemaObject | ReferenceObject => {
794
  if (isReferenceObject(depicted)) {
288✔
795
    return depicted;
6✔
796
  }
797
  const properties = depicted.properties
282✔
798
    ? omit(pathParams, depicted.properties)
799
    : undefined;
800
  const example = depicted.example
282✔
801
    ? omit(pathParams, depicted.example)
802
    : undefined;
803
  const required = depicted.required
282✔
804
    ? depicted.required.filter((name) => !pathParams.includes(name))
390✔
805
    : undefined;
806
  const allOf = depicted.allOf
282✔
807
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
808
        excludeParamsFromDepiction(entry, pathParams),
72✔
809
      )
810
    : undefined;
811
  const oneOf = depicted.oneOf
282✔
812
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
813
        excludeParamsFromDepiction(entry, pathParams),
36✔
814
      )
815
    : undefined;
816

817
  return omit(
282✔
818
    Object.entries({ properties, required, example, allOf, oneOf })
819
      .filter(([{}, value]) => value === undefined)
1,410✔
820
      .map(([key]) => key),
894✔
821
    {
822
      ...depicted,
823
      properties,
824
      required,
825
      example,
826
      allOf,
827
      oneOf,
828
    },
829
  );
830
};
831

832
export const excludeExampleFromDepiction = (
120✔
833
  depicted: SchemaObject | ReferenceObject,
834
): SchemaObject | ReferenceObject =>
835
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
882!
836

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

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

884
type SecurityHelper<K extends Security["type"]> = (
885
  security: Security & { type: K },
886
) => SecuritySchemeObject;
887

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

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

952
export const depictSecurityRefs = (
120✔
953
  container: LogicalContainer<{ name: string; scopes: string[] }>,
954
): SecurityRequirementObject[] => {
955
  if (typeof container === "object") {
684!
956
    if ("or" in container) {
684✔
957
      return container.or.map((entry) =>
354✔
958
        ("and" in entry
108✔
959
          ? entry.and
960
          : [entry]
961
        ).reduce<SecurityRequirementObject>(
962
          (agg, { name, scopes }) => ({
156✔
963
            ...agg,
964
            [name]: scopes,
965
          }),
966
          {},
967
        ),
968
      );
969
    }
970
    if ("and" in container) {
330✔
971
      return depictSecurityRefs(andToOr(container));
324✔
972
    }
973
  }
974
  return depictSecurityRefs({ or: [container] });
6✔
975
};
976

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

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

1028
export const depictTags = <TAG extends string>(
120✔
1029
  tags: TagsConfig<TAG>,
1030
): TagObject[] =>
1031
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
1032
    const def = tags[tag];
48✔
1033
    return {
48✔
1034
      name: tag,
1035
      description: typeof def === "string" ? def : def.description,
48✔
1036
      ...(typeof def === "object" &&
66✔
1037
        def.url && { externalDocs: { url: def.url } }),
1038
    };
1039
  });
1040

1041
export const ensureShortDescription = (description: string) => {
120✔
1042
  if (description.length <= shortDescriptionLimit) {
120✔
1043
    return description;
102✔
1044
  }
1045
  return description.slice(0, shortDescriptionLimit - 1) + "…";
18✔
1046
};
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