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

RobinTail / express-zod-api / 7434048251

06 Jan 2024 09:28PM CUT coverage: 100.0%. Remained the same
7434048251

Pull #1454

github

web-flow
Merge b3e42226e into cb74c3f05
Pull Request #1454: Bump @typescript-eslint/parser from 6.17.0 to 6.18.0

679 of 710 branches covered (0.0%)

1169 of 1169 relevant lines covered (100.0%)

460.85 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
  OAuthFlowObject,
7
  OAuthFlowsObject,
8
  ParameterObject,
9
  ReferenceObject,
10
  RequestBodyObject,
11
  ResponseObject,
12
  SchemaObject,
13
  SchemaObjectType,
14
  SecurityRequirementObject,
15
  SecuritySchemeObject,
16
  TagObject,
17
  isReferenceObject,
18
} from "openapi3-ts/oas31";
19
import { omit } from "ramda";
20
import { z } from "zod";
21
import {
22
  FlatObject,
23
  getExamples,
24
  hasCoercion,
25
  hasRaw,
26
  hasTopLevelTransformingEffect,
27
  isCustomHeader,
28
  makeCleanId,
29
  tryToTransform,
30
  ucFirst,
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 { DocumentationError } from "./errors";
36
import { ZodFile } from "./file-schema";
37
import { IOSchema } from "./io-schema";
38
import {
39
  LogicalContainer,
40
  andToOr,
41
  mapLogicalContainer,
42
} from "./logical-container";
43
import { copyMeta } from "./metadata";
44
import { Method } from "./method";
45
import {
46
  HandlingRules,
47
  HandlingVariant,
48
  SchemaHandler,
49
  walkSchema,
50
} from "./schema-walker";
51
import { Security } from "./security";
52
import { ZodUpload } from "./upload-schema";
53

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

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

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

73
interface ReqResDepictHelperCommonProps
74
  extends Pick<
75
    OpenAPIContext,
76
    "serializer" | "getRef" | "makeRef" | "path" | "method"
77
  > {
78
  schema: z.ZodTypeAny;
79
  mimeTypes: string[];
80
  composition: "inline" | "components";
81
  description?: string;
82
}
83

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

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

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

101
export const getRoutePathParams = (path: string): string[] => {
120✔
102
  const match = path.match(routePathParamsRegex);
708✔
103
  if (!match) {
708✔
104
    return [];
582✔
105
  }
106
  return match.map((param) => param.slice(1));
156✔
107
};
108

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

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

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

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

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

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

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

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

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

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

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

193
/** @since OAS 3.1 nullable replaced with type array having null */
194
export const depictNullable: Depicter<z.ZodNullable<z.ZodTypeAny>> = ({
120✔
195
  schema,
196
  next,
197
}) => {
198
  const nested = next({ schema: schema.unwrap() });
60✔
199
  if (!isReferenceObject(nested)) {
60!
200
    nested.type = makeNullableType(nested);
60✔
201
  }
202
  return nested;
60✔
203
};
204

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

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

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

242
/**
243
 * @see https://swagger.io/docs/specification/data-models/data-types/
244
 * @since OAS 3.1: using type: "null"
245
 * */
246
export const depictNull: Depicter<z.ZodNull> = () => ({ type: "null" });
120✔
247

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

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

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

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

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

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

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

377
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
120✔
378
  schema: { _def: def, element },
379
  next,
380
}) => {
381
  const result: SchemaObject = {
90✔
382
    type: "array",
383
    items: next({ schema: element }),
384
  };
385
  if (def.minLength) {
90✔
386
    result.minItems = def.minLength.value;
30✔
387
  }
388
  if (def.maxLength) {
90✔
389
    result.maxItems = def.maxLength.value;
6✔
390
  }
391
  return result;
90✔
392
};
393

394
/** @since OAS 3.1 using prefixItems for depicting tuples */
395
export const depictTuple: Depicter<z.ZodTuple> = ({
120✔
396
  schema: { items },
397
  next,
398
}) => {
399
  const types = items.map((item) => next({ schema: item }));
54✔
400
  return {
30✔
401
    type: "array",
402
    prefixItems: types,
403
  };
404
};
405

406
export const depictString: Depicter<z.ZodString> = ({
120✔
407
  schema: {
408
    isEmail,
409
    isURL,
410
    minLength,
411
    maxLength,
412
    isUUID,
413
    isCUID,
414
    isCUID2,
415
    isULID,
416
    isIP,
417
    isEmoji,
418
    isDatetime,
419
    _def: { checks },
420
  },
421
}) => {
422
  const regexCheck = checks.find(
1,242✔
423
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
424
      check.kind === "regex",
252✔
425
  );
426
  const datetimeCheck = checks.find(
1,242✔
427
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
428
      check.kind === "datetime",
258✔
429
  );
430
  const regex = regexCheck
1,242✔
431
    ? regexCheck.regex
432
    : datetimeCheck
1,188✔
433
      ? datetimeCheck.offset
12✔
434
        ? new RegExp(
435
            `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`,
436
          )
437
        : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
438
      : undefined;
439
  const result: SchemaObject = { type: "string" };
1,242✔
440
  const formats: Record<NonNullable<SchemaObject["format"]>, boolean> = {
1,242✔
441
    "date-time": isDatetime,
442
    email: isEmail,
443
    url: isURL,
444
    uuid: isUUID,
445
    cuid: isCUID,
446
    cuid2: isCUID2,
447
    ulid: isULID,
448
    ip: isIP,
449
    emoji: isEmoji,
450
  };
451
  for (const format in formats) {
1,242✔
452
    if (formats[format]) {
10,740✔
453
      result.format = format;
90✔
454
      break;
90✔
455
    }
456
  }
457
  if (minLength !== null) {
1,242✔
458
    result.minLength = minLength;
72✔
459
  }
460
  if (maxLength !== null) {
1,242✔
461
    result.maxLength = maxLength;
24✔
462
  }
463
  if (regex) {
1,242✔
464
    result.pattern = `/${regex.source}/${regex.flags}`;
66✔
465
  }
466
  return result;
1,242✔
467
};
468

469
/** @since OAS 3.1: exclusive min/max are numbers */
470
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
120✔
471
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
342✔
472
    | Extract<z.ZodNumberCheck, { kind: "min" }>
473
    | undefined;
474
  const minimum =
475
    schema.minValue === null
312✔
476
      ? schema.isInt
162✔
477
        ? Number.MIN_SAFE_INTEGER
478
        : Number.MIN_VALUE
479
      : schema.minValue;
480
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
312✔
481
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
366✔
482
    | Extract<z.ZodNumberCheck, { kind: "max" }>
483
    | undefined;
484
  const maximum =
485
    schema.maxValue === null
312✔
486
      ? schema.isInt
270✔
487
        ? Number.MAX_SAFE_INTEGER
488
        : Number.MAX_VALUE
489
      : schema.maxValue;
490
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
312✔
491
  const result: SchemaObject = {
312✔
492
    type: schema.isInt ? "integer" : "number",
312✔
493
    format: schema.isInt ? "int64" : "double",
312✔
494
  };
495
  if (isMinInclusive) {
312✔
496
    result.minimum = minimum;
234✔
497
  } else {
498
    result.exclusiveMinimum = minimum;
78✔
499
  }
500
  if (isMaxInclusive) {
312✔
501
    result.maximum = maximum;
294✔
502
  } else {
503
    result.exclusiveMaximum = maximum;
18✔
504
  }
505
  return result;
312✔
506
};
507

508
export const depictObjectProperties = ({
120✔
509
  schema: { shape },
510
  next,
511
}: Parameters<Depicter<z.AnyZodObject>>[0]) =>
512
  Object.keys(shape).reduce<Record<string, SchemaObject | ReferenceObject>>(
1,956✔
513
    (carry, key) => ({
3,042✔
514
      ...carry,
515
      [key]: next({ schema: shape[key] }),
516
    }),
517
    {},
518
  );
519

520
const makeSample = (depicted: SchemaObject) => {
120✔
521
  const type = (
522
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
42!
523
  ) as keyof typeof samples;
524
  return samples?.[type];
42✔
525
};
526

527
const makeNullableType = (prev: SchemaObject): SchemaObjectType[] => {
120✔
528
  const current = typeof prev.type === "string" ? [prev.type] : prev.type || [];
138!
529
  if (current.includes("null")) {
138✔
530
    return current;
72✔
531
  }
532
  return current.concat("null");
66✔
533
};
534

535
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
120✔
536
  schema,
537
  isResponse,
538
  next,
539
}) => {
540
  const input = next({ schema: schema.innerType() });
198✔
541
  const { effect } = schema._def;
198✔
542
  if (isResponse && effect.type === "transform" && !isReferenceObject(input)) {
198✔
543
    const outputType = tryToTransform(schema, makeSample(input));
42✔
544
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
42✔
545
      return { type: outputType as "number" | "string" | "boolean" };
30✔
546
    } else {
547
      return next({ schema: z.any() });
12✔
548
    }
549
  }
550
  if (
156✔
551
    !isResponse &&
324✔
552
    effect.type === "preprocess" &&
553
    !isReferenceObject(input)
554
  ) {
555
    const { type: inputType, ...rest } = input;
18✔
556
    return {
18✔
557
      ...rest,
558
      format: `${rest.format || inputType} (preprocessed)`,
30✔
559
    };
560
  }
561
  return input;
138✔
562
};
563

564
export const depictPipeline: Depicter<
565
  z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>
566
> = ({ schema, isResponse, next }) =>
120✔
567
  next({ schema: schema._def[isResponse ? "out" : "in"] });
12✔
568

569
export const depictBranded: Depicter<
570
  z.ZodBranded<z.ZodTypeAny, string | number | symbol>
571
> = ({ schema, next }) => next({ schema: schema.unwrap() });
120✔
572

573
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
120✔
574
  next,
575
  schema: lazy,
576
  serializer: serialize,
577
  getRef,
578
  makeRef,
579
}): ReferenceObject => {
580
  const hash = serialize(lazy.schema);
72✔
581
  return (
72✔
582
    getRef(hash) ||
108✔
583
    (() => {
584
      makeRef(hash, {}); // make empty ref first
36✔
585
      return makeRef(hash, next({ schema: lazy.schema })); // update
36✔
586
    })()
587
  );
588
};
589

590
export const depictExamples = (
120✔
591
  schema: z.ZodTypeAny,
592
  isResponse: boolean,
593
  omitProps: string[] = [],
828✔
594
): ExamplesObject | undefined => {
595
  const examples = getExamples({
1,026✔
596
    schema,
597
    variant: isResponse ? "parsed" : "original",
1,026✔
598
    validate: true,
599
  });
600
  if (examples.length === 0) {
1,026✔
601
    return undefined;
594✔
602
  }
603
  return examples.reduce<ExamplesObject>(
432✔
604
    (carry, example, index) => ({
450✔
605
      ...carry,
606
      [`example${index + 1}`]: {
607
        value:
608
          typeof example === "object" && !Array.isArray(example)
1,338✔
609
            ? omit(omitProps, example)
610
            : example,
611
      } satisfies ExampleObject,
612
    }),
613
    {},
614
  );
615
};
616

617
export const depictParamExamples = (
120✔
618
  schema: z.ZodTypeAny,
619
  isResponse: boolean,
620
  param: string,
621
): ExamplesObject | undefined => {
622
  const examples = getExamples({
240✔
623
    schema,
624
    variant: isResponse ? "parsed" : "original",
240✔
625
    validate: true,
626
  });
627
  if (examples.length === 0) {
240✔
628
    return undefined;
210✔
629
  }
630
  return examples.reduce<ExamplesObject>(
30✔
631
    (carry, example, index) =>
632
      param in example
42!
633
        ? {
634
            ...carry,
635
            [`example${index + 1}`]: {
636
              value: example[param],
637
            } satisfies ExampleObject,
638
          }
639
        : carry,
640
    {},
641
  );
642
};
643

644
export const extractObjectSchema = (
120✔
645
  subject: IOSchema,
646
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">,
647
) => {
648
  if (subject instanceof z.ZodObject) {
648✔
649
    return subject;
546✔
650
  }
651
  let objectSchema: z.AnyZodObject;
652
  if (
102✔
653
    subject instanceof z.ZodUnion ||
186✔
654
    subject instanceof z.ZodDiscriminatedUnion
655
  ) {
656
    objectSchema = Array.from(subject.options.values())
24✔
657
      .map((option) => extractObjectSchema(option, ctx))
48✔
658
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
48✔
659
  } else if (subject instanceof z.ZodEffects) {
78✔
660
    assert(
18✔
661
      !hasTopLevelTransformingEffect(subject),
662
      new DocumentationError({
663
        message: `Using transformations on the top level of ${
664
          ctx.isResponse ? "response" : "input"
18!
665
        } schema is not allowed.`,
666
        ...ctx,
667
      }),
668
    );
669
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
12✔
670
  } else {
671
    // intersection
672
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
60✔
673
      extractObjectSchema(subject._def.right, ctx),
674
    );
675
  }
676
  return copyMeta(subject, objectSchema);
96✔
677
};
678

679
export const depictRequestParams = ({
120✔
680
  path,
681
  method,
682
  schema,
683
  inputSources,
684
  serializer,
685
  getRef,
686
  makeRef,
687
  composition,
688
  description = `${method.toUpperCase()} ${path} Parameter`,
402✔
689
}: Omit<ReqResDepictHelperCommonProps, "mimeTypes"> & {
690
  inputSources: InputSource[];
691
}): ParameterObject[] => {
692
  const shape = extractObjectSchema(schema, {
414✔
693
    path,
694
    method,
695
    isResponse: false,
696
  }).shape;
697
  const pathParams = getRoutePathParams(path);
414✔
698
  const isQueryEnabled = inputSources.includes("query");
414✔
699
  const areParamsEnabled = inputSources.includes("params");
414✔
700
  const areHeadersEnabled = inputSources.includes("headers");
414✔
701
  const isPathParam = (name: string) =>
414✔
702
    areParamsEnabled && pathParams.includes(name);
696✔
703
  const isHeaderParam = (name: string) =>
414✔
704
    areHeadersEnabled && isCustomHeader(name);
174✔
705
  return Object.keys(shape)
414✔
706
    .filter((name) => isQueryEnabled || isPathParam(name))
660✔
707
    .map((name) => {
708
      const depicted = walkSchema({
228✔
709
        schema: shape[name],
710
        isResponse: false,
711
        rules: depicters,
712
        onEach,
713
        onMissing,
714
        serializer,
715
        getRef,
716
        makeRef,
717
        path,
718
        method,
719
      });
720
      const result =
721
        composition === "components"
228✔
722
          ? makeRef(makeCleanId(description, name), depicted)
723
          : depicted;
724
      return {
228✔
725
        name,
726
        in: isPathParam(name)
228✔
727
          ? "path"
728
          : isHeaderParam(name)
174✔
729
            ? "header"
730
            : "query",
731
        required: !shape[name].isOptional(),
732
        description: depicted.description || description,
432✔
733
        schema: result,
734
        examples: depictParamExamples(schema, false, name),
735
      };
736
    });
737
};
738

739
export const depicters: HandlingRules<
740
  SchemaObject | ReferenceObject,
741
  OpenAPIContext
742
> = {
120✔
743
  ZodString: depictString,
744
  ZodNumber: depictNumber,
745
  ZodBigInt: depictBigInt,
746
  ZodBoolean: depictBoolean,
747
  ZodDateIn: depictDateIn,
748
  ZodDateOut: depictDateOut,
749
  ZodNull: depictNull,
750
  ZodArray: depictArray,
751
  ZodTuple: depictTuple,
752
  ZodRecord: depictRecord,
753
  ZodObject: depictObject,
754
  ZodLiteral: depictLiteral,
755
  ZodIntersection: depictIntersection,
756
  ZodUnion: depictUnion,
757
  ZodFile: depictFile,
758
  ZodUpload: depictUpload,
759
  ZodAny: depictAny,
760
  ZodDefault: depictDefault,
761
  ZodEnum: depictEnum,
762
  ZodNativeEnum: depictEnum,
763
  ZodEffects: depictEffect,
764
  ZodOptional: depictOptional,
765
  ZodNullable: depictNullable,
766
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
767
  ZodBranded: depictBranded,
768
  ZodDate: depictDate,
769
  ZodCatch: depictCatch,
770
  ZodPipeline: depictPipeline,
771
  ZodLazy: depictLazy,
772
  ZodReadonly: depictReadonly,
773
};
774

775
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
120✔
776
  schema,
777
  isResponse,
778
  prev,
779
}) => {
780
  if (isReferenceObject(prev)) {
5,178✔
781
    return {};
54✔
782
  }
783
  const { description } = schema;
5,124✔
784
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,124✔
785
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,124✔
786
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,124✔
787
  const isActuallyNullable =
788
    !shouldAvoidParsing &&
5,124✔
789
    hasTypePropertyInDepiction &&
790
    !isResponseHavingCoercion &&
791
    schema.isNullable();
792
  const examples = shouldAvoidParsing
5,124!
793
    ? []
794
    : getExamples({
795
        schema,
796
        variant: isResponse ? "parsed" : "original",
5,124✔
797
        validate: true,
798
      });
799
  const result: SchemaObject = {};
5,124✔
800
  if (description) {
5,124✔
801
    result.description = description;
30✔
802
  }
803
  if (isActuallyNullable) {
5,124✔
804
    result.type = makeNullableType(prev);
78✔
805
  }
806
  if (examples.length) {
5,124✔
807
    result.examples = Array.from(examples);
504✔
808
  }
809
  return result;
5,124✔
810
};
811

812
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({ schema, ...ctx }) =>
120✔
813
  assert.fail(
42✔
814
    new DocumentationError({
815
      message: `Zod type ${schema.constructor.name} is unsupported.`,
816
      ...ctx,
817
    }),
818
  );
819

820
export const excludeParamsFromDepiction = (
120✔
821
  depicted: SchemaObject | ReferenceObject,
822
  pathParams: string[],
823
): SchemaObject | ReferenceObject => {
824
  if (isReferenceObject(depicted)) {
318✔
825
    return depicted;
6✔
826
  }
827
  const properties = depicted.properties
312✔
828
    ? omit(pathParams, depicted.properties)
829
    : undefined;
830
  const examples = depicted.examples
312✔
831
    ? depicted.examples.map((entry) => omit(pathParams, entry))
66✔
832
    : undefined;
833
  const required = depicted.required
312✔
834
    ? depicted.required.filter((name) => !pathParams.includes(name))
432✔
835
    : undefined;
836
  const allOf = depicted.allOf
312✔
837
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
838
        excludeParamsFromDepiction(entry, pathParams),
72✔
839
      )
840
    : undefined;
841
  const oneOf = depicted.oneOf
312✔
842
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
843
        excludeParamsFromDepiction(entry, pathParams),
36✔
844
      )
845
    : undefined;
846

847
  return omit(
312✔
848
    Object.entries({ properties, required, examples, allOf, oneOf })
849
      .filter(([{}, value]) => value === undefined)
1,560✔
850
      .map(([key]) => key),
984✔
851
    {
852
      ...depicted,
853
      properties,
854
      required,
855
      examples,
856
      allOf,
857
      oneOf,
858
    },
859
  );
860
};
861

862
export const excludeExamplesFromDepiction = (
120✔
863
  depicted: SchemaObject | ReferenceObject,
864
): SchemaObject | ReferenceObject =>
865
  isReferenceObject(depicted) ? depicted : omit(["examples"], depicted);
1,020!
866

867
export const depictResponse = ({
120✔
868
  method,
869
  path,
870
  schema,
871
  mimeTypes,
872
  variant,
873
  serializer,
874
  getRef,
875
  makeRef,
876
  composition,
877
  hasMultipleStatusCodes,
878
  statusCode,
879
  description = `${method.toUpperCase()} ${path} ${ucFirst(variant)} response ${
804✔
880
    hasMultipleStatusCodes ? statusCode : ""
804✔
881
  }`.trim(),
882
}: ReqResDepictHelperCommonProps & {
883
  variant: "positive" | "negative";
884
  statusCode: number;
885
  hasMultipleStatusCodes: boolean;
886
}): ResponseObject => {
887
  const depictedSchema = excludeExamplesFromDepiction(
828✔
888
    walkSchema({
889
      schema,
890
      isResponse: true,
891
      rules: depicters,
892
      onEach,
893
      onMissing,
894
      serializer,
895
      getRef,
896
      makeRef,
897
      path,
898
      method,
899
    }),
900
  );
901
  const examples = depictExamples(schema, true);
828✔
902
  const result =
903
    composition === "components"
828✔
904
      ? makeRef(makeCleanId(description), depictedSchema)
905
      : depictedSchema;
906

907
  return {
828✔
908
    description,
909
    content: mimeTypes.reduce<ContentObject>(
910
      (carry, mimeType) => ({
834✔
911
        ...carry,
912
        [mimeType]: { schema: result, examples },
913
      }),
914
      {},
915
    ),
916
  };
917
};
918

919
type SecurityHelper<K extends Security["type"]> = (
920
  security: Security & { type: K },
921
  inputSources?: InputSource[],
922
) => SecuritySchemeObject;
923

924
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
120✔
925
  type: "http",
926
  scheme: "basic",
927
});
928
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
120✔
929
  format: bearerFormat,
930
}) => {
931
  const result: SecuritySchemeObject = {
24✔
932
    type: "http",
933
    scheme: "bearer",
934
  };
935
  if (bearerFormat) {
24✔
936
    result.bearerFormat = bearerFormat;
6✔
937
  }
938
  return result;
24✔
939
};
940
const depictInputSecurity: SecurityHelper<"input"> = (
120✔
941
  { name },
942
  inputSources,
943
) => {
944
  const result: SecuritySchemeObject = {
36✔
945
    type: "apiKey",
946
    in: "query",
947
    name,
948
  };
949
  if (inputSources?.includes("body")) {
36✔
950
    if (inputSources?.includes("query")) {
24✔
951
      result["x-in-alternative"] = "body";
6✔
952
      result.description = `${name} CAN also be supplied within the request body`;
6✔
953
    } else {
954
      result["x-in-actual"] = "body";
18✔
955
      result.description = `${name} MUST be supplied within the request body instead of query`;
18✔
956
    }
957
  }
958
  return result;
36✔
959
};
960
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
120✔
961
  type: "apiKey",
962
  in: "header",
963
  name,
964
});
965
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
120✔
966
  type: "apiKey",
967
  in: "cookie",
968
  name,
969
});
970
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
120✔
971
  url: openIdConnectUrl,
972
}) => ({
6✔
973
  type: "openIdConnect",
974
  openIdConnectUrl,
975
});
976
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
120✔
977
  type: "oauth2",
978
  flows: (
979
    Object.keys(flows) as (keyof typeof flows)[]
980
  ).reduce<OAuthFlowsObject>((acc, key) => {
981
    const flow = flows[key];
42✔
982
    if (!flow) {
42✔
983
      return acc;
12✔
984
    }
985
    const { scopes = {}, ...rest } = flow;
30!
986
    return { ...acc, [key]: { ...rest, scopes } satisfies OAuthFlowObject };
30✔
987
  }, {}),
988
});
989

990
export const depictSecurity = (
120✔
991
  container: LogicalContainer<Security>,
992
  inputSources?: InputSource[],
993
): LogicalContainer<SecuritySchemeObject> => {
994
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
390✔
995
    basic: depictBasicSecurity,
996
    bearer: depictBearerSecurity,
997
    input: depictInputSecurity,
998
    header: depictHeaderSecurity,
999
    cookie: depictCookieSecurity,
1000
    openid: depictOpenIdSecurity,
1001
    oauth2: depictOAuth2Security,
1002
  };
1003
  return mapLogicalContainer(container, (security) =>
390✔
1004
    (methods[security.type] as SecurityHelper<typeof security.type>)(
120✔
1005
      security,
1006
      inputSources,
1007
    ),
1008
  );
1009
};
1010

1011
export const depictSecurityRefs = (
120✔
1012
  container: LogicalContainer<{ name: string; scopes: string[] }>,
1013
): SecurityRequirementObject[] => {
1014
  if (typeof container === "object") {
744!
1015
    if ("or" in container) {
744✔
1016
      return container.or.map((entry) =>
384✔
1017
        ("and" in entry
108✔
1018
          ? entry.and
1019
          : [entry]
1020
        ).reduce<SecurityRequirementObject>(
1021
          (agg, { name, scopes }) => ({
156✔
1022
            ...agg,
1023
            [name]: scopes,
1024
          }),
1025
          {},
1026
        ),
1027
      );
1028
    }
1029
    if ("and" in container) {
360✔
1030
      return depictSecurityRefs(andToOr(container));
354✔
1031
    }
1032
  }
1033
  return depictSecurityRefs({ or: [container] });
6✔
1034
};
1035

1036
export const depictRequest = ({
120✔
1037
  method,
1038
  path,
1039
  schema,
1040
  mimeTypes,
1041
  serializer,
1042
  getRef,
1043
  makeRef,
1044
  composition,
1045
  description = `${method.toUpperCase()} ${path} Request body`,
216✔
1046
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
1047
  const pathParams = getRoutePathParams(path);
228✔
1048
  const bodyDepiction = excludeExamplesFromDepiction(
228✔
1049
    excludeParamsFromDepiction(
1050
      walkSchema({
1051
        schema: hasRaw(schema) ? ZodFile.create().buffer() : schema,
228✔
1052
        isResponse: false,
1053
        rules: depicters,
1054
        onEach,
1055
        onMissing,
1056
        serializer,
1057
        getRef,
1058
        makeRef,
1059
        path,
1060
        method,
1061
      }),
1062
      pathParams,
1063
    ),
1064
  );
1065
  const bodyExamples = depictExamples(schema, false, pathParams);
186✔
1066
  const result =
1067
    composition === "components"
186✔
1068
      ? makeRef(makeCleanId(description), bodyDepiction)
1069
      : bodyDepiction;
1070

1071
  return {
186✔
1072
    description,
1073
    content: mimeTypes.reduce<ContentObject>(
1074
      (carry, mimeType) => ({
186✔
1075
        ...carry,
1076
        [mimeType]: { schema: result, examples: bodyExamples },
1077
      }),
1078
      {},
1079
    ),
1080
  };
1081
};
1082

1083
export const depictTags = <TAG extends string>(
120✔
1084
  tags: TagsConfig<TAG>,
1085
): TagObject[] =>
1086
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
1087
    const def = tags[tag];
48✔
1088
    const result: TagObject = {
48✔
1089
      name: tag,
1090
      description: typeof def === "string" ? def : def.description,
48✔
1091
    };
1092
    if (typeof def === "object" && def.url) {
48✔
1093
      result.externalDocs = { url: def.url };
6✔
1094
    }
1095
    return result;
48✔
1096
  });
1097

1098
export const ensureShortDescription = (description: string) => {
120✔
1099
  if (description.length <= shortDescriptionLimit) {
120✔
1100
    return description;
102✔
1101
  }
1102
  return description.slice(0, shortDescriptionLimit - 1) + "…";
18✔
1103
};
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