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

RobinTail / express-zod-api / 7171933085

11 Dec 2023 06:43PM UTC coverage: 100.0%. Remained the same
7171933085

Pull #1373

github

web-flow
Merge 25081b5c5 into 6b01f5c67
Pull Request #1373: Bump @typescript-eslint/eslint-plugin from 6.13.2 to 6.14.0

673 of 701 branches covered (0.0%)

1075 of 1075 relevant lines covered (100.0%)

358.32 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;
100✔
87
const isoDateDocumentationUrl =
88
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
100✔
89

90
const samples = {
100✔
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;
100✔
102

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

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

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

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

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

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

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

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

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

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

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

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

195
export const depictNullable: Depicter<z.ZodNullable<z.ZodTypeAny>> = ({
100✔
196
  schema,
197
  next,
198
}) => ({
30✔
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 }) => ({
100✔
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>> = ({
100✔
211
  schema: { value },
212
}) => ({
615✔
213
  type: typeof value as "string" | "number" | "boolean",
214
  enum: [value],
215
});
216

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

247
export const depictDateIn: Depicter<ZodDateIn> = (ctx) => {
100✔
248
  assert(
25✔
249
    !ctx.isResponse,
250
    new DocumentationError({
251
      message: "Please use z.dateOut() for output.",
252
      ...ctx,
253
    }),
254
  );
255
  return {
20✔
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) => {
100✔
267
  assert(
25✔
268
    ctx.isResponse,
269
    new DocumentationError({
270
      message: "Please use z.dateIn() for input.",
271
      ...ctx,
272
    }),
273
  );
274
  return {
20✔
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) =>
100✔
286
  assert.fail(
10✔
287
    new DocumentationError({
288
      message: `Using z.date() within ${
289
        ctx.isResponse ? "output" : "input"
10✔
290
      } schema is forbidden. Please use z.date${
291
        ctx.isResponse ? "Out" : "In"
10✔
292
      }() instead. Check out the documentation for details.`,
293
      ...ctx,
294
    }),
295
  );
296

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

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

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

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

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

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

408
export const depictString: Depicter<z.ZodString> = ({
100✔
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(
975✔
425
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
426
      check.kind === "regex",
200✔
427
  );
428
  const datetimeCheck = checks.find(
975✔
429
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
430
      check.kind === "datetime",
205✔
431
  );
432
  const regex = regexCheck
975✔
433
    ? regexCheck.regex
434
    : datetimeCheck
930✔
435
      ? datetimeCheck.offset
10✔
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 {
975✔
442
    type: "string" as const,
443
    ...(isDatetime && { format: "date-time" }),
985✔
444
    ...(isEmail && { format: "email" }),
990✔
445
    ...(isURL && { format: "url" }),
985✔
446
    ...(isUUID && { format: "uuid" }),
985✔
447
    ...(isCUID && { format: "cuid" }),
985✔
448
    ...(isCUID2 && { format: "cuid2" }),
980✔
449
    ...(isULID && { format: "ulid" }),
980✔
450
    ...(isIP && { format: "ip" }),
980✔
451
    ...(isEmoji && { format: "emoji" }),
980✔
452
    ...(minLength !== null && { minLength }),
1,025✔
453
    ...(maxLength !== null && { maxLength }),
995✔
454
    ...(regex && { pattern: `/${regex.source}/${regex.flags}` }),
1,030✔
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 }) => {
100✔
460
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
235✔
461
    | Extract<z.ZodNumberCheck, { kind: "min" }>
462
    | undefined;
463
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
225✔
464
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
255✔
465
    | Extract<z.ZodNumberCheck, { kind: "max" }>
466
    | undefined;
467
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
225✔
468
  return {
225✔
469
    type: schema.isInt ? ("integer" as const) : ("number" as const),
225✔
470
    format: schema.isInt ? ("int64" as const) : ("double" as const),
225✔
471
    minimum:
472
      schema.minValue === null
225✔
473
        ? schema.isInt
120✔
474
          ? Number.MIN_SAFE_INTEGER
475
          : Number.MIN_VALUE
476
        : schema.minValue,
477
    exclusiveMinimum: !isMinInclusive,
478
    maximum:
479
      schema.maxValue === null
225✔
480
        ? schema.isInt
190✔
481
          ? Number.MAX_SAFE_INTEGER
482
          : Number.MAX_VALUE
483
        : schema.maxValue,
484
    exclusiveMaximum: !isMaxInclusive,
485
  };
486
};
487

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

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

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

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

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

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

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

588
export const depictParamExamples = (
100✔
589
  schema: z.ZodTypeAny,
590
  isResponse: boolean,
591
  param: string,
592
): MediaExamples => {
593
  const examples = getExamples({
190✔
594
    schema,
595
    variant: isResponse ? "parsed" : "original",
190✔
596
    validate: true,
597
  });
598
  if (examples.length === 0) {
190✔
599
    return {};
165✔
600
  }
601
  return {
25✔
602
    examples: examples.reduce<ExamplesObject>(
603
      (carry, example, index) =>
604
        param in example
35!
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 = (
100✔
618
  subject: IOSchema,
619
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">,
620
) => {
621
  if (subject instanceof z.ZodObject) {
520✔
622
    return subject;
435✔
623
  }
624
  let objectSchema: z.AnyZodObject;
625
  if (
85✔
626
    subject instanceof z.ZodUnion ||
155✔
627
    subject instanceof z.ZodDiscriminatedUnion
628
  ) {
629
    objectSchema = Array.from(subject.options.values())
20✔
630
      .map((option) => extractObjectSchema(option, ctx))
40✔
631
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
40✔
632
  } else if (subject instanceof z.ZodEffects) {
65✔
633
    assert(
15✔
634
      !hasTopLevelTransformingEffect(subject),
635
      new DocumentationError({
636
        message: `Using transformations on the top level of ${
637
          ctx.isResponse ? "response" : "input"
15!
638
        } schema is not allowed.`,
639
        ...ctx,
640
      }),
641
    );
642
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
10✔
643
  } else {
644
    // intersection
645
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
50✔
646
      extractObjectSchema(subject._def.right, ctx),
647
    );
648
  }
649
  return copyMeta(subject, objectSchema);
80✔
650
};
651

652
export const depictRequestParams = ({
100✔
653
  path,
654
  method,
655
  endpoint,
656
  inputSources,
657
  serializer,
658
  getRef,
659
  makeRef,
660
  composition,
661
  clue = "parameter",
325✔
662
}: ReqResDepictHelperCommonProps & {
663
  inputSources: InputSource[];
664
}): ParameterObject[] => {
665
  const schema = endpoint.getSchema("input");
325✔
666
  const shape = extractObjectSchema(schema, {
325✔
667
    path,
668
    method,
669
    isResponse: false,
670
  }).shape;
671
  const pathParams = getRoutePathParams(path);
325✔
672
  const isQueryEnabled = inputSources.includes("query");
325✔
673
  const areParamsEnabled = inputSources.includes("params");
325✔
674
  const areHeadersEnabled = inputSources.includes("headers");
325✔
675
  const isPathParam = (name: string) =>
325✔
676
    areParamsEnabled && pathParams.includes(name);
535✔
677
  const isHeaderParam = (name: string) =>
325✔
678
    areHeadersEnabled && isCustomHeader(name);
145✔
679
  return Object.keys(shape)
325✔
680
    .filter((name) => isQueryEnabled || isPathParam(name))
515✔
681
    .map((name) => {
682
      const depicted = walkSchema({
180✔
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"
180✔
696
          ? makeRef(makeCleanId(path, method, `${clue} ${name}`), depicted)
697
          : depicted;
698
      return {
180✔
699
        name,
700
        in: isPathParam(name)
180✔
701
          ? "path"
702
          : isHeaderParam(name)
145✔
703
            ? "header"
704
            : "query",
705
        required: !shape[name].isOptional(),
706
        description:
707
          (isSchemaObject(depicted) && depicted.description) ||
520✔
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
> = {
100✔
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"> = ({
100✔
752
  schema,
753
  isResponse,
754
  prev,
755
}) => {
756
  if (isReferenceObject(prev)) {
3,895✔
757
    return {};
45✔
758
  }
759
  const { description } = schema;
3,850✔
760
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
3,850✔
761
  const hasTypePropertyInDepiction = prev.type !== undefined;
3,850✔
762
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
3,850✔
763
  const isActuallyNullable =
764
    !shouldAvoidParsing &&
3,850✔
765
    hasTypePropertyInDepiction &&
766
    !isResponseHavingCoercion &&
767
    schema.isNullable();
768
  const examples = shouldAvoidParsing
3,850!
769
    ? []
770
    : getExamples({
771
        schema,
772
        variant: isResponse ? "parsed" : "original",
3,850✔
773
        validate: true,
774
      });
775
  return {
3,850✔
776
    ...(description && { description }),
3,875✔
777
    ...(isActuallyNullable && { nullable: true }),
3,905✔
778
    ...(examples.length > 0 && { example: examples[0] }),
4,260✔
779
  };
780
};
781

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

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

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

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

837
export const depictResponse = ({
100✔
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");
600✔
851
  const mimeTypes = endpoint.getMimeTypes(isPositive ? "positive" : "negative");
600✔
852
  const depictedSchema = excludeExampleFromDepiction(
600✔
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);
600✔
867
  const result =
868
    composition === "components"
600✔
869
      ? makeRef(makeCleanId(path, method, clue), depictedSchema)
870
      : depictedSchema;
871

872
  return {
600✔
873
    description: `${method.toUpperCase()} ${path} ${clue}`,
874
    content: mimeTypes.reduce(
875
      (carry, mimeType) => ({
605✔
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"> = () => ({
100✔
889
  type: "http",
890
  scheme: "basic",
891
});
892
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
100✔
893
  format: bearerFormat,
894
}) => ({
20✔
895
  type: "http",
896
  scheme: "bearer",
897
  ...(bearerFormat && { bearerFormat }),
25✔
898
});
899
// @todo add description on actual input placement
900
const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
100✔
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 }) => ({
100✔
906
  type: "apiKey",
907
  in: "header",
908
  name,
909
});
910
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
100✔
911
  type: "apiKey",
912
  in: "cookie",
913
  name,
914
});
915
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
100✔
916
  url: openIdConnectUrl,
917
}) => ({
5✔
918
  type: "openIdConnect",
919
  openIdConnectUrl,
920
});
921
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
100✔
922
  type: "oauth2",
923
  flows: (
924
    Object.keys(flows) as (keyof typeof flows)[]
925
  ).reduce<OAuthFlowsObject>((acc, key) => {
926
    const flow = flows[key];
35✔
927
    if (!flow) {
35✔
928
      return acc;
10✔
929
    }
930
    const { scopes = {}, ...rest } = flow;
25!
931
    return { ...acc, [key]: { ...rest, scopes } };
25✔
932
  }, {}),
933
});
934

935
export const depictSecurity = (
100✔
936
  container: LogicalContainer<Security>,
937
): LogicalContainer<SecuritySchemeObject> => {
938
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
290✔
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) =>
290✔
948
    (methods[security.type] as SecurityHelper<typeof security.type>)(security),
90✔
949
  );
950
};
951

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

977
export const depictRequest = ({
100✔
978
  method,
979
  path,
980
  endpoint,
981
  serializer,
982
  getRef,
983
  makeRef,
984
  composition,
985
  clue = "request body",
165✔
986
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
987
  const pathParams = getRoutePathParams(path);
165✔
988
  const inputSchema = endpoint.getSchema("input");
165✔
989
  const bodyDepiction = excludeExampleFromDepiction(
165✔
990
    excludeParamsFromDepiction(
991
      walkSchema({
992
        schema: hasRaw(inputSchema) ? ZodFile.create().buffer() : inputSchema,
165✔
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(
130✔
1007
    endpoint.getSchema("input"),
1008
    false,
1009
    pathParams,
1010
  );
1011
  const result =
1012
    composition === "components"
130✔
1013
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
1014
      : bodyDepiction;
1015

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

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

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