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

RobinTail / express-zod-api / 7379556135

01 Jan 2024 07:43PM CUT coverage: 100.0%. Remained the same
7379556135

Pull #1437

github

web-flow
Merge e5c5cd95d into a9e15f4f1
Pull Request #1437: Bump @typescript-eslint/parser from 6.16.0 to 6.17.0

684 of 716 branches covered (0.0%)

1186 of 1186 relevant lines covered (100.0%)

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

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

74
interface ReqResDepictHelperCommonProps
75
  extends Pick<
76
    OpenAPIContext,
77
    "serializer" | "getRef" | "makeRef" | "path" | "method"
78
  > {
79
  endpoint: AbstractEndpoint;
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);
672✔
103
  if (!match) {
672✔
104
    return [];
546✔
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)}}`);
354✔
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 z.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
}) => ({
810✔
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,788✔
225
    const prop = schema.shape[key];
2,772✔
226
    const isOptional =
227
      isResponse && hasCoercion(prop)
2,772✔
228
        ? prop instanceof z.ZodOptional
229
        : prop.isOptional();
230
    return !isOptional;
2,772✔
231
  });
232
  const result: SchemaObject = {
1,788✔
233
    type: "object",
234
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
235
  };
236
  if (required.length) {
1,746✔
237
    result.required = required;
1,560✔
238
  }
239
  return result;
1,746✔
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 z.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 z.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 z.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,194✔
423
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
424
      check.kind === "regex",
240✔
425
  );
426
  const datetimeCheck = checks.find(
1,194✔
427
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
428
      check.kind === "datetime",
246✔
429
  );
430
  const regex = regexCheck
1,194✔
431
    ? regexCheck.regex
432
    : datetimeCheck
1,140✔
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,194✔
440
  const formats: Record<NonNullable<SchemaObject["format"]>, boolean> = {
1,194✔
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,194✔
452
    if (formats[format]) {
10,308✔
453
      result.format = format;
90✔
454
      break;
90✔
455
    }
456
  }
457
  if (minLength !== null) {
1,194✔
458
    result.minLength = minLength;
60✔
459
  }
460
  if (maxLength !== null) {
1,194✔
461
    result.maxLength = maxLength;
24✔
462
  }
463
  if (regex) {
1,194✔
464
    result.pattern = `/${regex.source}/${regex.flags}`;
66✔
465
  }
466
  return result;
1,194✔
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
282✔
472
    | Extract<z.ZodNumberCheck, { kind: "min" }>
473
    | undefined;
474
  const minimum =
475
    schema.minValue === null
270✔
476
      ? schema.isInt
144✔
477
        ? Number.MIN_SAFE_INTEGER
478
        : Number.MIN_VALUE
479
      : schema.minValue;
480
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
270✔
481
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
306✔
482
    | Extract<z.ZodNumberCheck, { kind: "max" }>
483
    | undefined;
484
  const maximum =
485
    schema.maxValue === null
270✔
486
      ? schema.isInt
228✔
487
        ? Number.MAX_SAFE_INTEGER
488
        : Number.MAX_VALUE
489
      : schema.maxValue;
490
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
270✔
491
  const result: SchemaObject = {
270✔
492
    type: schema.isInt ? "integer" : "number",
270✔
493
    format: schema.isInt ? "int64" : "double",
270✔
494
  };
495
  if (isMinInclusive) {
270✔
496
    result.minimum = minimum;
216✔
497
  } else {
498
    result.exclusiveMinimum = minimum;
54✔
499
  }
500
  if (isMaxInclusive) {
270✔
501
    result.maximum = maximum;
252✔
502
  } else {
503
    result.exclusiveMaximum = maximum;
18✔
504
  }
505
  return result;
270✔
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,830✔
513
    (carry, key) => ({
2,844✔
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[] = [],
744✔
594
): ExamplesObject | undefined => {
595
  const examples = getExamples({
924✔
596
    schema,
597
    variant: isResponse ? "parsed" : "original",
924✔
598
    validate: true,
599
  });
600
  if (examples.length === 0) {
924✔
601
    return undefined;
492✔
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) {
630✔
649
    return subject;
528✔
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
  endpoint,
683
  inputSources,
684
  serializer,
685
  getRef,
686
  makeRef,
687
  composition,
688
  description = `${method.toUpperCase()} ${path} Parameter`,
384✔
689
}: ReqResDepictHelperCommonProps & {
690
  inputSources: InputSource[];
691
}): ParameterObject[] => {
692
  const schema = endpoint.getSchema("input");
396✔
693
  const shape = extractObjectSchema(schema, {
396✔
694
    path,
695
    method,
696
    isResponse: false,
697
  }).shape;
698
  const pathParams = getRoutePathParams(path);
396✔
699
  const isQueryEnabled = inputSources.includes("query");
396✔
700
  const areParamsEnabled = inputSources.includes("params");
396✔
701
  const areHeadersEnabled = inputSources.includes("headers");
396✔
702
  const isPathParam = (name: string) =>
396✔
703
    areParamsEnabled && pathParams.includes(name);
678✔
704
  const isHeaderParam = (name: string) =>
396✔
705
    areHeadersEnabled && isCustomHeader(name);
174✔
706
  return Object.keys(shape)
396✔
707
    .filter((name) => isQueryEnabled || isPathParam(name))
642✔
708
    .map((name) => {
709
      const depicted = walkSchema({
228✔
710
        schema: shape[name],
711
        isResponse: false,
712
        rules: depicters,
713
        onEach,
714
        onMissing,
715
        serializer,
716
        getRef,
717
        makeRef,
718
        path,
719
        method,
720
      });
721
      const result =
722
        composition === "components"
228✔
723
          ? makeRef(makeCleanId(description, name), depicted)
724
          : depicted;
725
      return {
228✔
726
        name,
727
        in: isPathParam(name)
228✔
728
          ? "path"
729
          : isHeaderParam(name)
174✔
730
            ? "header"
731
            : "query",
732
        required: !shape[name].isOptional(),
733
        description: depicted.description || description,
432✔
734
        schema: result,
735
        examples: depictParamExamples(schema, false, name),
736
      };
737
    });
738
};
739

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

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

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

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

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

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

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

903
  return {
744✔
904
    description,
905
    content: mimeTypes.reduce<ContentObject>(
906
      (carry, mimeType) => ({
750✔
907
        ...carry,
908
        [mimeType]: { schema: result, examples },
909
      }),
910
      {},
911
    ),
912
  };
913
};
914

915
type SecurityHelper<K extends Security["type"]> = (
916
  security: Security & { type: K },
917
  inputSources?: InputSource[],
918
) => SecuritySchemeObject;
919

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

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

1007
export const depictSecurityRefs = (
120✔
1008
  container: LogicalContainer<{ name: string; scopes: string[] }>,
1009
): SecurityRequirementObject[] => {
1010
  if (typeof container === "object") {
708!
1011
    if ("or" in container) {
708✔
1012
      return container.or.map((entry) =>
366✔
1013
        ("and" in entry
108✔
1014
          ? entry.and
1015
          : [entry]
1016
        ).reduce<SecurityRequirementObject>(
1017
          (agg, { name, scopes }) => ({
156✔
1018
            ...agg,
1019
            [name]: scopes,
1020
          }),
1021
          {},
1022
        ),
1023
      );
1024
    }
1025
    if ("and" in container) {
342✔
1026
      return depictSecurityRefs(andToOr(container));
336✔
1027
    }
1028
  }
1029
  return depictSecurityRefs({ or: [container] });
6✔
1030
};
1031

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

1071
  return {
168✔
1072
    description,
1073
    content: endpoint.getMimeTypes("input").reduce<ContentObject>(
1074
      (carry, mimeType) => ({
168✔
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