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

RobinTail / express-zod-api / 9338230058

02 Jun 2024 12:26PM CUT coverage: 100.0%. Remained the same
9338230058

Pull #1806

github

web-flow
Merge f4435aad8 into 54d4837b9
Pull Request #1806: Bump typescript-eslint from 8.0.0-alpha.20 to 8.0.0-alpha.24 in the typescript-eslint group

687 of 721 branches covered (95.28%)

1130 of 1130 relevant lines covered (100.0%)

474.27 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
  ExamplesObject,
4
  MediaTypeObject,
5
  OAuthFlowObject,
6
  ParameterLocation,
7
  ParameterObject,
8
  ReferenceObject,
9
  RequestBodyObject,
10
  ResponseObject,
11
  SchemaObject,
12
  SchemaObjectType,
13
  SecurityRequirementObject,
14
  SecuritySchemeObject,
15
  TagObject,
16
  isReferenceObject,
17
} from "openapi3-ts/oas31";
18
import {
19
  both,
20
  complement,
21
  concat,
22
  type as detectType,
23
  filter,
24
  fromPairs,
25
  has,
26
  isNil,
27
  map,
28
  mergeAll,
29
  mergeDeepRight,
30
  mergeDeepWith,
31
  objOf,
32
  omit,
33
  pipe,
34
  pluck,
35
  range,
36
  reject,
37
  toLower,
38
  union,
39
  when,
40
  xprod,
41
  zip,
42
} from "ramda";
43
import { z } from "zod";
44
import {
45
  FlatObject,
46
  combinations,
47
  getExamples,
48
  hasCoercion,
49
  isCustomHeader,
50
  isObject,
51
  makeCleanId,
52
  tryToTransform,
53
  ucFirst,
54
} from "./common-helpers";
55
import { InputSource, TagsConfig } from "./config-type";
56
import { DateInSchema, ezDateInBrand } from "./date-in-schema";
57
import { DateOutSchema, ezDateOutBrand } from "./date-out-schema";
58
import { DocumentationError } from "./errors";
59
import { FileSchema, ezFileBrand } from "./file-schema";
60
import { IOSchema } from "./io-schema";
61
import {
62
  LogicalContainer,
63
  andToOr,
64
  mapLogicalContainer,
65
} from "./logical-container";
66
import { metaSymbol } from "./metadata";
67
import { Method } from "./method";
68
import { ProprietaryBrand } from "./proprietary-schemas";
69
import { RawSchema, ezRawBrand } from "./raw-schema";
70
import { HandlingRules, SchemaHandler, walkSchema } from "./schema-walker";
71
import { Security } from "./security";
72
import { UploadSchema, ezUploadBrand } from "./upload-schema";
73

74
export interface OpenAPIContext extends FlatObject {
75
  isResponse: boolean;
76
  serializer: (schema: z.ZodTypeAny) => string;
77
  getRef: (name: string) => ReferenceObject | undefined;
78
  makeRef: (
79
    name: string,
80
    schema: SchemaObject | ReferenceObject,
81
  ) => ReferenceObject;
82
  path: string;
83
  method: Method;
84
}
85

86
export type Depicter = SchemaHandler<
87
  SchemaObject | ReferenceObject,
88
  OpenAPIContext
89
>;
90

91
interface ReqResDepictHelperCommonProps
92
  extends Pick<
93
    OpenAPIContext,
94
    "serializer" | "getRef" | "makeRef" | "path" | "method"
95
  > {
96
  schema: z.ZodTypeAny;
97
  mimeTypes: ReadonlyArray<string>;
98
  composition: "inline" | "components";
99
  description?: string;
100
  brandHandling?: HandlingRules<SchemaObject | ReferenceObject, OpenAPIContext>;
101
}
102

103
const shortDescriptionLimit = 50;
156βœ”
104
const isoDateDocumentationUrl =
105
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
156βœ”
106

107
const samples = {
156βœ”
108
  integer: 0,
109
  number: 0,
110
  string: "",
111
  boolean: false,
112
  object: {},
113
  null: null,
114
  array: [],
115
} satisfies Record<Extract<SchemaObjectType, string>, unknown>;
116

117
/** @see https://expressjs.com/en/guide/routing.html */
118
const routePathParamsRegex = /:([A-Za-z0-9_]+)/g;
156βœ”
119

120
export const getRoutePathParams = (path: string): string[] =>
156βœ”
121
  path.match(routePathParamsRegex)?.map((param) => param.slice(1)) || [];
498βœ”
122

123
export const reformatParamsInPath = (path: string) =>
156βœ”
124
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
390βœ”
125

126
export const depictDefault: Depicter = (
156βœ”
127
  { _def }: z.ZodDefault<z.ZodTypeAny>,
128
  { next },
129
) => ({
24βœ”
130
  ...next(_def.innerType),
131
  default: _def[metaSymbol]?.defaultLabel || _def.defaultValue(),
42βœ”
132
});
133

134
export const depictCatch: Depicter = (
156βœ”
135
  { _def: { innerType } }: z.ZodCatch<z.ZodTypeAny>,
136
  { next },
137
) => next(innerType);
6βœ”
138

139
export const depictAny: Depicter = () => ({ format: "any" });
156βœ”
140

141
export const depictUpload: Depicter = ({}: UploadSchema, ctx) => {
156βœ”
142
  assert(
24βœ”
143
    !ctx.isResponse,
144
    new DocumentationError({
145
      message: "Please use ez.upload() only for input.",
146
      ...ctx,
147
    }),
148
  );
149
  return { type: "string", format: "binary" };
18βœ”
150
};
151

152
export const depictFile: Depicter = (schema: FileSchema) => {
156βœ”
153
  const subject = schema.unwrap();
54βœ”
154
  return {
54βœ”
155
    type: "string",
156
    format:
157
      subject instanceof z.ZodString
54βœ”
158
        ? subject._def.checks.find((check) => check.kind === "base64")
6βœ”
159
          ? "byte"
160
          : "file"
161
        : "binary",
162
  };
163
};
164

165
export const depictUnion: Depicter = (
156βœ”
166
  { options }: z.ZodUnion<z.ZodUnionOptions>,
167
  { next },
168
) => ({ oneOf: options.map(next) });
78βœ”
169

170
export const depictDiscriminatedUnion: Depicter = (
156βœ”
171
  {
172
    options,
173
    discriminator,
174
  }: z.ZodDiscriminatedUnion<string, z.ZodDiscriminatedUnionOption<string>[]>,
175
  { next },
176
) => {
177
  return {
18βœ”
178
    discriminator: { propertyName: discriminator },
179
    oneOf: options.map(next),
180
  };
181
};
182

183
/** @throws AssertionError */
184
const tryFlattenIntersection = (
156βœ”
185
  children: Array<SchemaObject | ReferenceObject>,
186
) => {
187
  const [left, right] = children.filter(
96βœ”
188
    (entry): entry is SchemaObject =>
189
      !isReferenceObject(entry) &&
192βœ”
190
      entry.type === "object" &&
191
      Object.keys(entry).every((key) =>
192
        ["type", "properties", "required", "examples"].includes(key),
552βœ”
193
      ),
194
  );
195
  assert(left && right, "Can not flatten objects");
96βœ”
196
  const flat: SchemaObject = { type: "object" };
78βœ”
197
  if (left.properties || right.properties) {
78βœ”
198
    flat.properties = mergeDeepWith(
66βœ”
199
      (a, b) =>
200
        Array.isArray(a) && Array.isArray(b)
18βœ”
201
          ? concat(a, b)
202
          : a === b
12!
203
            ? b
204
            : assert.fail("Can not flatten properties"),
205
      left.properties || {},
66!
206
      right.properties || {},
66!
207
    );
208
  }
209
  if (left.required || right.required) {
78βœ”
210
    flat.required = union(left.required || [], right.required || []);
66!
211
  }
212
  if (left.examples || right.examples) {
78βœ”
213
    flat.examples = combinations(
36βœ”
214
      left.examples || [],
36!
215
      right.examples || [],
36!
216
      ([a, b]) => mergeDeepRight(a, b),
36βœ”
217
    );
218
  }
219
  return flat;
78βœ”
220
};
221

222
export const depictIntersection: Depicter = (
156βœ”
223
  { _def: { left, right } }: z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>,
224
  { next },
225
) => {
226
  const children = [left, right].map(next);
96βœ”
227
  try {
96βœ”
228
    return tryFlattenIntersection(children);
96βœ”
229
  } catch {}
230
  return { allOf: children };
18βœ”
231
};
232

233
export const depictOptional: Depicter = (
156βœ”
234
  schema: z.ZodOptional<z.ZodTypeAny>,
235
  { next },
236
) => next(schema.unwrap());
66βœ”
237

238
export const depictReadonly: Depicter = (
156βœ”
239
  schema: z.ZodReadonly<z.ZodTypeAny>,
240
  { next },
241
) => next(schema.unwrap());
12βœ”
242

243
/** @since OAS 3.1 nullable replaced with type array having null */
244
export const depictNullable: Depicter = (
156βœ”
245
  schema: z.ZodNullable<z.ZodTypeAny>,
246
  { next },
247
) => {
248
  const nested = next(schema.unwrap());
60βœ”
249
  if (!isReferenceObject(nested)) {
60!
250
    nested.type = makeNullableType(nested);
60βœ”
251
  }
252
  return nested;
60βœ”
253
};
254

255
const getSupportedType = (value: unknown): SchemaObjectType | undefined => {
156βœ”
256
  const detected = toLower(detectType(value)); // toLower is typed well unlike .toLowerCase()
984βœ”
257
  const isSupported =
258
    detected === "number" ||
984βœ”
259
    detected === "string" ||
260
    detected === "boolean" ||
261
    detected === "object" ||
262
    detected === "null" ||
263
    detected === "array";
264
  return typeof value === "bigint"
984βœ”
265
    ? "integer"
266
    : isSupported
978βœ”
267
      ? detected
268
      : undefined;
269
};
270

271
export const depictEnum: Depicter = (
156βœ”
272
  schema: z.ZodEnum<[string, ...string[]]> | z.ZodNativeEnum<z.EnumLike>,
273
) => ({
24βœ”
274
  type: getSupportedType(Object.values(schema.enum)[0]),
275
  enum: Object.values(schema.enum),
276
});
277

278
export const depictLiteral: Depicter = ({ value }: z.ZodLiteral<unknown>) => ({
960βœ”
279
  type: getSupportedType(value), // constructor allows z.Primitive only, but ZodLiteral does not have that constrant
280
  const: value,
281
});
282

283
export const depictObject: Depicter = (
156βœ”
284
  schema: z.ZodObject<z.ZodRawShape>,
285
  { isResponse, next },
286
) => {
287
  const keys = Object.keys(schema.shape);
2,052βœ”
288
  const isOptionalProp = (prop: z.ZodTypeAny) =>
2,052βœ”
289
    isResponse && hasCoercion(prop)
3,144βœ”
290
      ? prop instanceof z.ZodOptional
291
      : prop.isOptional();
292
  const required = keys.filter((key) => !isOptionalProp(schema.shape[key]));
3,144βœ”
293
  const result: SchemaObject = { type: "object" };
2,052βœ”
294
  if (keys.length) {
2,052βœ”
295
    result.properties = depictObjectProperties(schema, next);
1,872βœ”
296
  }
297
  if (required.length) {
2,010βœ”
298
    result.required = required;
1,812βœ”
299
  }
300
  return result;
2,010βœ”
301
};
302

303
/**
304
 * @see https://swagger.io/docs/specification/data-models/data-types/
305
 * @since OAS 3.1: using type: "null"
306
 * */
307
export const depictNull: Depicter = () => ({ type: "null" });
156βœ”
308

309
export const depictDateIn: Depicter = ({}: DateInSchema, ctx) => {
156βœ”
310
  assert(
30βœ”
311
    !ctx.isResponse,
312
    new DocumentationError({
313
      message: "Please use ez.dateOut() for output.",
314
      ...ctx,
315
    }),
316
  );
317
  return {
24βœ”
318
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
319
    type: "string",
320
    format: "date-time",
321
    pattern: /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?)?Z?$/.source,
322
    externalDocs: {
323
      url: isoDateDocumentationUrl,
324
    },
325
  };
326
};
327

328
export const depictDateOut: Depicter = ({}: DateOutSchema, ctx) => {
156βœ”
329
  assert(
30βœ”
330
    ctx.isResponse,
331
    new DocumentationError({
332
      message: "Please use ez.dateIn() for input.",
333
      ...ctx,
334
    }),
335
  );
336
  return {
24βœ”
337
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
338
    type: "string",
339
    format: "date-time",
340
    externalDocs: {
341
      url: isoDateDocumentationUrl,
342
    },
343
  };
344
};
345

346
/** @throws DocumentationError */
347
export const depictDate: Depicter = ({}: z.ZodDate, ctx) =>
156βœ”
348
  assert.fail(
12βœ”
349
    new DocumentationError({
350
      message: `Using z.date() within ${
351
        ctx.isResponse ? "output" : "input"
12βœ”
352
      } schema is forbidden. Please use ez.date${
353
        ctx.isResponse ? "Out" : "In"
12βœ”
354
      }() instead. Check out the documentation for details.`,
355
      ...ctx,
356
    }),
357
  );
358

359
export const depictBoolean: Depicter = () => ({ type: "boolean" });
210βœ”
360

361
export const depictBigInt: Depicter = () => ({
156βœ”
362
  type: "integer",
363
  format: "bigint",
364
});
365

366
const areOptionsLiteral = (
156βœ”
367
  subject: z.ZodTypeAny[],
368
): subject is z.ZodLiteral<unknown>[] =>
369
  subject.every((option) => option instanceof z.ZodLiteral);
24βœ”
370

371
export const depictRecord: Depicter = (
156βœ”
372
  { keySchema, valueSchema }: z.ZodRecord<z.ZodTypeAny>,
373
  { next },
374
) => {
375
  if (keySchema instanceof z.ZodEnum || keySchema instanceof z.ZodNativeEnum) {
108βœ”
376
    const keys = Object.values(keySchema.enum) as string[];
12βœ”
377
    const result: SchemaObject = { type: "object" };
12βœ”
378
    if (keys.length) {
12!
379
      result.properties = depictObjectProperties(
12βœ”
380
        z.object(fromPairs(xprod(keys, [valueSchema]))),
381
        next,
382
      );
383
      result.required = keys;
12βœ”
384
    }
385
    return result;
12βœ”
386
  }
387
  if (keySchema instanceof z.ZodLiteral) {
96βœ”
388
    return {
24βœ”
389
      type: "object",
390
      properties: depictObjectProperties(
391
        z.object({ [keySchema.value]: valueSchema }),
392
        next,
393
      ),
394
      required: [keySchema.value],
395
    };
396
  }
397
  if (keySchema instanceof z.ZodUnion && areOptionsLiteral(keySchema.options)) {
72βœ”
398
    const required = map((opt) => `${opt.value}`, keySchema.options);
24βœ”
399
    const shape = fromPairs(xprod(required, [valueSchema]));
12βœ”
400
    return {
12βœ”
401
      type: "object",
402
      properties: depictObjectProperties(z.object(shape), next),
403
      required,
404
    };
405
  }
406
  return { type: "object", additionalProperties: next(valueSchema) };
60βœ”
407
};
408

409
export const depictArray: Depicter = (
156βœ”
410
  { _def: { minLength, maxLength }, element }: z.ZodArray<z.ZodTypeAny>,
411
  { next },
412
) => {
413
  const result: SchemaObject = { type: "array", items: next(element) };
90βœ”
414
  if (minLength) {
90βœ”
415
    result.minItems = minLength.value;
30βœ”
416
  }
417
  if (maxLength) {
90βœ”
418
    result.maxItems = maxLength.value;
6βœ”
419
  }
420
  return result;
90βœ”
421
};
422

423
/**
424
 * @since OAS 3.1 using prefixItems for depicting tuples
425
 * @since 17.5.0 added rest handling, fixed tuple type
426
 * */
427
export const depictTuple: Depicter = (
156βœ”
428
  { items, _def: { rest } }: z.AnyZodTuple,
429
  { next },
430
) => ({
42βœ”
431
  type: "array",
432
  prefixItems: items.map(next),
433
  // does not appear to support items:false, so not:{} is a recommended alias
434
  items: rest === null ? { not: {} } : next(rest),
42βœ”
435
});
436

437
export const depictString: Depicter = ({
156βœ”
438
  isEmail,
439
  isURL,
440
  minLength,
441
  maxLength,
442
  isUUID,
443
  isCUID,
444
  isCUID2,
445
  isULID,
446
  isIP,
447
  isEmoji,
448
  isDatetime,
449
  _def: { checks },
450
}: z.ZodString) => {
451
  const regexCheck = checks.find(
1,326βœ”
452
    (check): check is Extract<z.ZodStringCheck, { kind: "regex" }> =>
453
      check.kind === "regex",
258βœ”
454
  );
455
  const datetimeCheck = checks.find(
1,326βœ”
456
    (check): check is Extract<z.ZodStringCheck, { kind: "datetime" }> =>
457
      check.kind === "datetime",
264βœ”
458
  );
459
  const regex = regexCheck
1,326βœ”
460
    ? regexCheck.regex
461
    : datetimeCheck
1,272βœ”
462
      ? datetimeCheck.offset
18βœ”
463
        ? new RegExp(
464
            `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`,
465
          )
466
        : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
467
      : undefined;
468
  const result: SchemaObject = { type: "string" };
1,326βœ”
469
  const formats: Record<NonNullable<SchemaObject["format"]>, boolean> = {
1,326βœ”
470
    "date-time": isDatetime,
471
    email: isEmail,
472
    url: isURL,
473
    uuid: isUUID,
474
    cuid: isCUID,
475
    cuid2: isCUID2,
476
    ulid: isULID,
477
    ip: isIP,
478
    emoji: isEmoji,
479
  };
480
  for (const format in formats) {
1,326βœ”
481
    if (formats[format]) {
11,448βœ”
482
      result.format = format;
96βœ”
483
      break;
96βœ”
484
    }
485
  }
486
  if (minLength !== null) {
1,326βœ”
487
    result.minLength = minLength;
72βœ”
488
  }
489
  if (maxLength !== null) {
1,326βœ”
490
    result.maxLength = maxLength;
24βœ”
491
  }
492
  if (regex) {
1,326βœ”
493
    result.pattern = regex.source;
72βœ”
494
  }
495
  return result;
1,326βœ”
496
};
497

498
/** @since OAS 3.1: exclusive min/max are numbers */
499
export const depictNumber: Depicter = ({
156βœ”
500
  isInt,
501
  maxValue,
502
  minValue,
503
  _def: { checks },
504
}: z.ZodNumber) => {
505
  const minCheck = checks.find(({ kind }) => kind === "min") as
366βœ”
506
    | Extract<z.ZodNumberCheck, { kind: "min" }>
507
    | undefined;
508
  const minimum =
509
    minValue === null
366βœ”
510
      ? isInt
216βœ”
511
        ? Number.MIN_SAFE_INTEGER
512
        : -Number.MAX_VALUE
513
      : minValue;
514
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
366βœ”
515
  const maxCheck = checks.find(({ kind }) => kind === "max") as
366βœ”
516
    | Extract<z.ZodNumberCheck, { kind: "max" }>
517
    | undefined;
518
  const maximum =
519
    maxValue === null
366βœ”
520
      ? isInt
324βœ”
521
        ? Number.MAX_SAFE_INTEGER
522
        : Number.MAX_VALUE
523
      : maxValue;
524
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
366βœ”
525
  const result: SchemaObject = {
366βœ”
526
    type: isInt ? "integer" : "number",
366βœ”
527
    format: isInt ? "int64" : "double",
366βœ”
528
  };
529
  if (isMinInclusive) {
366βœ”
530
    result.minimum = minimum;
288βœ”
531
  } else {
532
    result.exclusiveMinimum = minimum;
78βœ”
533
  }
534
  if (isMaxInclusive) {
366βœ”
535
    result.maximum = maximum;
348βœ”
536
  } else {
537
    result.exclusiveMaximum = maximum;
18βœ”
538
  }
539
  return result;
366βœ”
540
};
541

542
export const depictObjectProperties = (
156βœ”
543
  { shape }: z.ZodObject<z.ZodRawShape>,
544
  next: Parameters<Depicter>[1]["next"],
545
) => map(next, shape);
1,926βœ”
546

547
const makeSample = (depicted: SchemaObject) => {
156βœ”
548
  const firstType = (
549
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
42!
550
  ) as keyof typeof samples;
551
  return samples?.[firstType];
42βœ”
552
};
553

554
const makeNullableType = (prev: SchemaObject): SchemaObjectType[] => {
156βœ”
555
  const current = typeof prev.type === "string" ? [prev.type] : prev.type || [];
138!
556
  if (current.includes("null")) {
138βœ”
557
    return current;
72βœ”
558
  }
559
  return current.concat("null");
66βœ”
560
};
561

562
export const depictEffect: Depicter = (
156βœ”
563
  schema: z.ZodEffects<z.ZodTypeAny>,
564
  { isResponse, next },
565
) => {
566
  const input = next(schema.innerType());
186βœ”
567
  const { effect } = schema._def;
186βœ”
568
  if (isResponse && effect.type === "transform" && !isReferenceObject(input)) {
186βœ”
569
    const outputType = tryToTransform(schema, makeSample(input));
42βœ”
570
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
42βœ”
571
      return { type: outputType as "number" | "string" | "boolean" };
30βœ”
572
    } else {
573
      return next(z.any());
12βœ”
574
    }
575
  }
576
  if (
144βœ”
577
    !isResponse &&
300βœ”
578
    effect.type === "preprocess" &&
579
    !isReferenceObject(input)
580
  ) {
581
    const { type: inputType, ...rest } = input;
18βœ”
582
    return {
18βœ”
583
      ...rest,
584
      format: `${rest.format || inputType} (preprocessed)`,
30βœ”
585
    };
586
  }
587
  return input;
126βœ”
588
};
589

590
export const depictPipeline: Depicter = (
156βœ”
591
  { _def }: z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>,
592
  { isResponse, next },
593
) => next(_def[isResponse ? "out" : "in"]);
12βœ”
594

595
export const depictBranded: Depicter = (
156βœ”
596
  schema: z.ZodBranded<z.ZodTypeAny, string | number | symbol>,
597
  { next },
598
) => next(schema.unwrap());
6βœ”
599

600
export const depictLazy: Depicter = (
156βœ”
601
  { schema }: z.ZodLazy<z.ZodTypeAny>,
602
  { next, serializer: serialize, getRef, makeRef },
603
): ReferenceObject => {
604
  const hash = serialize(schema);
72βœ”
605
  return (
72βœ”
606
    getRef(hash) ||
108βœ”
607
    (() => {
608
      makeRef(hash, {}); // make empty ref first
36βœ”
609
      return makeRef(hash, next(schema)); // update
36βœ”
610
    })()
611
  );
612
};
613

614
export const depictRaw: Depicter = (schema: RawSchema, { next }) =>
156βœ”
615
  next(schema.unwrap().shape.raw);
12βœ”
616

617
const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined =>
156βœ”
618
  examples.length
1,344βœ”
619
    ? fromPairs(
620
        zip(
621
          range(1, examples.length + 1).map((idx) => `example${idx}`),
498βœ”
622
          map(objOf("value"), examples),
623
        ),
624
      )
625
    : undefined;
626

627
export const depictExamples = (
156βœ”
628
  schema: z.ZodTypeAny,
629
  isResponse: boolean,
630
  omitProps: string[] = [],
864βœ”
631
): ExamplesObject | undefined =>
632
  pipe(
1,074βœ”
633
    getExamples,
634
    map(when(both(isObject, complement(Array.isArray)), omit(omitProps))),
635
    enumerateExamples,
636
  )({ schema, variant: isResponse ? "parsed" : "original", validate: true });
1,074βœ”
637

638
export const depictParamExamples = (
156βœ”
639
  schema: z.ZodTypeAny,
640
  param: string,
641
): ExamplesObject | undefined =>
642
  pipe(
270βœ”
643
    getExamples,
644
    filter<FlatObject>(has(param)),
645
    pluck(param),
646
    enumerateExamples,
647
  )({ schema, variant: "original", validate: true });
648

649
export const extractObjectSchema = (
156βœ”
650
  subject: IOSchema,
651
  tfError: DocumentationError,
652
): z.ZodObject<z.ZodRawShape> => {
653
  if (subject instanceof z.ZodObject) {
636βœ”
654
    return subject;
534βœ”
655
  }
656
  if (subject instanceof z.ZodBranded) {
102βœ”
657
    return extractObjectSchema(subject.unwrap(), tfError);
18βœ”
658
  }
659
  if (
84βœ”
660
    subject instanceof z.ZodUnion ||
156βœ”
661
    subject instanceof z.ZodDiscriminatedUnion
662
  ) {
663
    return subject.options
18βœ”
664
      .map((option) => extractObjectSchema(option, tfError))
36βœ”
665
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
36βœ”
666
  } else if (subject instanceof z.ZodEffects) {
66βœ”
667
    assert(subject._def.effect.type === "refinement", tfError);
12βœ”
668
    return extractObjectSchema(subject._def.schema, tfError); // object refinement
6βœ”
669
  } // intersection left
670
  return extractObjectSchema(subject._def.left, tfError).merge(
54βœ”
671
    extractObjectSchema(subject._def.right, tfError),
672
  );
673
};
674

675
export const depictRequestParams = ({
156βœ”
676
  path,
677
  method,
678
  schema,
679
  inputSources,
680
  serializer,
681
  getRef,
682
  makeRef,
683
  composition,
684
  brandHandling,
685
  description = `${method.toUpperCase()} ${path} Parameter`,
420βœ”
686
}: Omit<ReqResDepictHelperCommonProps, "mimeTypes"> & {
687
  inputSources: InputSource[];
688
}) => {
689
  const { shape } = extractObjectSchema(
432βœ”
690
    schema,
691
    new DocumentationError({
692
      message: `Using transformations on the top level schema is not allowed.`,
693
      path,
694
      method,
695
      isResponse: false,
696
    }),
697
  );
698
  const pathParams = getRoutePathParams(path);
432βœ”
699
  const isQueryEnabled = inputSources.includes("query");
432βœ”
700
  const areParamsEnabled = inputSources.includes("params");
432βœ”
701
  const areHeadersEnabled = inputSources.includes("headers");
432βœ”
702
  const isPathParam = (name: string) =>
432βœ”
703
    areParamsEnabled && pathParams.includes(name);
702βœ”
704
  const isHeaderParam = (name: string) =>
432βœ”
705
    areHeadersEnabled && isCustomHeader(name);
642βœ”
706

707
  const parameters = Object.keys(shape)
432βœ”
708
    .map<{ name: string; location?: ParameterLocation }>((name) => ({
702βœ”
709
      name,
710
      location: isPathParam(name)
702βœ”
711
        ? "path"
712
        : isHeaderParam(name)
642βœ”
713
          ? "header"
714
          : isQueryEnabled
618βœ”
715
            ? "query"
716
            : undefined,
717
    }))
718
    .filter(
719
      (parameter): parameter is Required<typeof parameter> =>
720
        parameter.location !== undefined,
702βœ”
721
    );
722

723
  return parameters.map<ParameterObject>(({ name, location }) => {
432βœ”
724
    const depicted = walkSchema(shape[name], {
264βœ”
725
      rules: { ...brandHandling, ...depicters },
726
      onEach,
727
      onMissing,
728
      ctx: {
729
        isResponse: false,
730
        serializer,
731
        getRef,
732
        makeRef,
733
        path,
734
        method,
735
      },
736
    });
737
    const result =
738
      composition === "components"
264βœ”
739
        ? makeRef(makeCleanId(description, name), depicted)
740
        : depicted;
741
    return {
264βœ”
742
      name,
743
      in: location,
744
      required: !shape[name].isOptional(),
745
      description: depicted.description || description,
504βœ”
746
      schema: result,
747
      examples: depictParamExamples(schema, name),
748
    };
749
  });
750
};
751

752
export const depicters: HandlingRules<
753
  SchemaObject | ReferenceObject,
754
  OpenAPIContext,
755
  z.ZodFirstPartyTypeKind | ProprietaryBrand
756
> = {
156βœ”
757
  ZodString: depictString,
758
  ZodNumber: depictNumber,
759
  ZodBigInt: depictBigInt,
760
  ZodBoolean: depictBoolean,
761
  ZodNull: depictNull,
762
  ZodArray: depictArray,
763
  ZodTuple: depictTuple,
764
  ZodRecord: depictRecord,
765
  ZodObject: depictObject,
766
  ZodLiteral: depictLiteral,
767
  ZodIntersection: depictIntersection,
768
  ZodUnion: depictUnion,
769
  ZodAny: depictAny,
770
  ZodDefault: depictDefault,
771
  ZodEnum: depictEnum,
772
  ZodNativeEnum: depictEnum,
773
  ZodEffects: depictEffect,
774
  ZodOptional: depictOptional,
775
  ZodNullable: depictNullable,
776
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
777
  ZodBranded: depictBranded,
778
  ZodDate: depictDate,
779
  ZodCatch: depictCatch,
780
  ZodPipeline: depictPipeline,
781
  ZodLazy: depictLazy,
782
  ZodReadonly: depictReadonly,
783
  [ezFileBrand]: depictFile,
784
  [ezUploadBrand]: depictUpload,
785
  [ezDateOutBrand]: depictDateOut,
786
  [ezDateInBrand]: depictDateIn,
787
  [ezRawBrand]: depictRaw,
788
};
789

790
export const onEach: SchemaHandler<
791
  SchemaObject | ReferenceObject,
792
  OpenAPIContext,
793
  "each"
794
> = (schema, { isResponse, prev }) => {
156βœ”
795
  if (isReferenceObject(prev)) {
5,574βœ”
796
    return {};
54βœ”
797
  }
798
  const { description } = schema;
5,520βœ”
799
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,520βœ”
800
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,520βœ”
801
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,520βœ”
802
  const isActuallyNullable =
803
    !shouldAvoidParsing &&
5,520βœ”
804
    hasTypePropertyInDepiction &&
805
    !isResponseHavingCoercion &&
806
    schema.isNullable();
807
  const examples = shouldAvoidParsing
5,520!
808
    ? []
809
    : getExamples({
810
        schema,
811
        variant: isResponse ? "parsed" : "original",
5,520βœ”
812
        validate: true,
813
      });
814
  const result: SchemaObject = {};
5,520βœ”
815
  if (description) {
5,520βœ”
816
    result.description = description;
30βœ”
817
  }
818
  if (isActuallyNullable) {
5,520βœ”
819
    result.type = makeNullableType(prev);
78βœ”
820
  }
821
  if (examples.length) {
5,520βœ”
822
    result.examples = examples.slice();
552βœ”
823
  }
824
  return result;
5,520βœ”
825
};
826

827
export const onMissing: SchemaHandler<
828
  SchemaObject | ReferenceObject,
829
  OpenAPIContext,
830
  "last"
831
> = (schema, ctx) =>
156βœ”
832
  assert.fail(
42βœ”
833
    new DocumentationError({
834
      message: `Zod type ${schema.constructor.name} is unsupported.`,
835
      ...ctx,
836
    }),
837
  );
838

839
export const excludeParamsFromDepiction = (
156βœ”
840
  depicted: SchemaObject | ReferenceObject,
841
  names: string[],
842
): SchemaObject | ReferenceObject => {
843
  if (isReferenceObject(depicted)) {
276βœ”
844
    return depicted;
6βœ”
845
  }
846
  const copy = { ...depicted };
270βœ”
847
  if (copy.properties) {
270βœ”
848
    copy.properties = omit(names, copy.properties);
204βœ”
849
  }
850
  if (copy.examples) {
270βœ”
851
    copy.examples = copy.examples.map((entry) => omit(names, entry));
30βœ”
852
  }
853
  if (copy.required) {
270βœ”
854
    copy.required = copy.required.filter((name) => !names.includes(name));
462βœ”
855
  }
856
  if (copy.allOf) {
270βœ”
857
    copy.allOf = copy.allOf.map((entry) =>
6βœ”
858
      excludeParamsFromDepiction(entry, names),
12βœ”
859
    );
860
  }
861
  if (copy.oneOf) {
270βœ”
862
    copy.oneOf = copy.oneOf.map((entry) =>
18βœ”
863
      excludeParamsFromDepiction(entry, names),
36βœ”
864
    );
865
  }
866
  return copy;
270βœ”
867
};
868

869
export const excludeExamplesFromDepiction = (
156βœ”
870
  depicted: SchemaObject | ReferenceObject,
871
): SchemaObject | ReferenceObject =>
872
  isReferenceObject(depicted) ? depicted : omit(["examples"], depicted);
1,068!
873

874
export const depictResponse = ({
156βœ”
875
  method,
876
  path,
877
  schema,
878
  mimeTypes,
879
  variant,
880
  serializer,
881
  getRef,
882
  makeRef,
883
  composition,
884
  hasMultipleStatusCodes,
885
  statusCode,
886
  brandHandling,
887
  description = `${method.toUpperCase()} ${path} ${ucFirst(variant)} response ${
840βœ”
888
    hasMultipleStatusCodes ? statusCode : ""
840βœ”
889
  }`.trim(),
890
}: ReqResDepictHelperCommonProps & {
891
  variant: "positive" | "negative";
892
  statusCode: number;
893
  hasMultipleStatusCodes: boolean;
894
}): ResponseObject => {
895
  const depictedSchema = excludeExamplesFromDepiction(
864βœ”
896
    walkSchema(schema, {
897
      rules: { ...brandHandling, ...depicters },
898
      onEach,
899
      onMissing,
900
      ctx: {
901
        isResponse: true,
902
        serializer,
903
        getRef,
904
        makeRef,
905
        path,
906
        method,
907
      },
908
    }),
909
  );
910
  const media: MediaTypeObject = {
864βœ”
911
    schema:
912
      composition === "components"
864βœ”
913
        ? makeRef(makeCleanId(description), depictedSchema)
914
        : depictedSchema,
915
    examples: depictExamples(schema, true),
916
  };
917
  return { description, content: fromPairs(xprod(mimeTypes, [media])) };
864βœ”
918
};
919

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

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

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

1006
export const depictSecurityRefs = (
156βœ”
1007
  container: LogicalContainer<{ name: string; scopes: string[] }>,
1008
): SecurityRequirementObject[] => {
1009
  if ("or" in container) {
780βœ”
1010
    return container.or.map(
402βœ”
1011
      (entry): SecurityRequirementObject =>
1012
        "and" in entry
108βœ”
1013
          ? mergeAll(map(({ name, scopes }) => objOf(name, scopes), entry.and))
90βœ”
1014
          : { [entry.name]: entry.scopes },
1015
    );
1016
  }
1017
  if ("and" in container) {
378βœ”
1018
    return depictSecurityRefs(andToOr(container));
372βœ”
1019
  }
1020
  return depictSecurityRefs({ or: [container] });
6βœ”
1021
};
1022

1023
export const depictBody = ({
156βœ”
1024
  method,
1025
  path,
1026
  schema,
1027
  mimeTypes,
1028
  serializer,
1029
  getRef,
1030
  makeRef,
1031
  composition,
1032
  brandHandling,
1033
  paramNames,
1034
  description = `${method.toUpperCase()} ${path} Request body`,
228βœ”
1035
}: ReqResDepictHelperCommonProps & {
1036
  paramNames: string[];
1037
}): RequestBodyObject => {
1038
  const bodyDepiction = excludeExamplesFromDepiction(
240βœ”
1039
    excludeParamsFromDepiction(
1040
      walkSchema(schema, {
1041
        rules: { ...brandHandling, ...depicters },
1042
        onEach,
1043
        onMissing,
1044
        ctx: {
1045
          isResponse: false,
1046
          serializer,
1047
          getRef,
1048
          makeRef,
1049
          path,
1050
          method,
1051
        },
1052
      }),
1053
      paramNames,
1054
    ),
1055
  );
1056
  const media: MediaTypeObject = {
198βœ”
1057
    schema:
1058
      composition === "components"
198βœ”
1059
        ? makeRef(makeCleanId(description), bodyDepiction)
1060
        : bodyDepiction,
1061
    examples: depictExamples(schema, false, paramNames),
1062
  };
1063
  return { description, content: fromPairs(xprod(mimeTypes, [media])) };
198βœ”
1064
};
1065

1066
export const depictTags = <TAG extends string>(
156βœ”
1067
  tags: TagsConfig<TAG>,
1068
): TagObject[] =>
1069
  (Object.keys(tags) as TAG[]).map((tag) => {
24βœ”
1070
    const def = tags[tag];
48βœ”
1071
    const result: TagObject = {
48βœ”
1072
      name: tag,
1073
      description: typeof def === "string" ? def : def.description,
48βœ”
1074
    };
1075
    if (typeof def === "object" && def.url) {
48βœ”
1076
      result.externalDocs = { url: def.url };
6βœ”
1077
    }
1078
    return result;
48βœ”
1079
  });
1080

1081
export const ensureShortDescription = (description: string) =>
156βœ”
1082
  description.length <= shortDescriptionLimit
126βœ”
1083
    ? description
1084
    : description.slice(0, shortDescriptionLimit - 1) + "…";
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