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

RobinTail / express-zod-api / 11692394271

05 Nov 2024 08:57PM CUT coverage: 100.0%. First build
11692394271

Pull #2151

github

web-flow
Merge f09d18e40 into e39fbfbb4
Pull Request #2151: Bump the typescript-eslint group with 2 updates

1143 of 1163 branches covered (98.28%)

3990 of 3990 relevant lines covered (100.0%)

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

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

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

92
interface ReqResHandlingProps<S extends z.ZodTypeAny>
93
  extends Pick<OpenAPIContext, "makeRef" | "path" | "method"> {
94
  schema: S;
95
  composition: "inline" | "components";
96
  description?: string;
97
  brandHandling?: HandlingRules<SchemaObject | ReferenceObject, OpenAPIContext>;
98
}
99

100
const shortDescriptionLimit = 50;
6✔
101
const isoDateDocumentationUrl =
6✔
102
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
6✔
103

104
const samples = {
6✔
105
  integer: 0,
6✔
106
  number: 0,
6✔
107
  string: "",
6✔
108
  boolean: false,
6✔
109
  object: {},
6✔
110
  null: null,
6✔
111
  array: [],
6✔
112
} satisfies Record<Extract<SchemaObjectType, string>, unknown>;
6✔
113

114
/** @see https://expressjs.com/en/guide/routing.html */
115
const routePathParamsRegex = /:([A-Za-z0-9_]+)/g;
6✔
116

117
export const getRoutePathParams = (path: string): string[] =>
6✔
118
  path.match(routePathParamsRegex)?.map((param) => param.slice(1)) || [];
528✔
119

120
export const reformatParamsInPath = (path: string) =>
6✔
121
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
402✔
122

123
export const depictDefault: Depicter = (
6✔
124
  { _def }: z.ZodDefault<z.ZodTypeAny>,
24✔
125
  { next },
24✔
126
) => ({
24✔
127
  ...next(_def.innerType),
24✔
128
  default: _def[metaSymbol]?.defaultLabel || _def.defaultValue(),
24✔
129
});
24✔
130

131
export const depictCatch: Depicter = (
6✔
132
  { _def: { innerType } }: z.ZodCatch<z.ZodTypeAny>,
6✔
133
  { next },
6✔
134
) => next(innerType);
6✔
135

136
export const depictAny: Depicter = () => ({ format: "any" });
6✔
137

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

149
export const depictFile: Depicter = (schema: FileSchema) => {
6✔
150
  const subject = schema.unwrap();
54✔
151
  return {
54✔
152
    type: "string",
54✔
153
    format:
54✔
154
      subject instanceof z.ZodString
54✔
155
        ? subject._def.checks.find((check) => check.kind === "base64")
18✔
156
          ? "byte"
6✔
157
          : "file"
12✔
158
        : "binary",
36✔
159
  };
54✔
160
};
54✔
161

162
export const depictUnion: Depicter = (
6✔
163
  { options }: z.ZodUnion<z.ZodUnionOptions>,
78✔
164
  { next },
78✔
165
) => ({ oneOf: options.map(next) });
78✔
166

167
export const depictDiscriminatedUnion: Depicter = (
6✔
168
  {
18✔
169
    options,
18✔
170
    discriminator,
18✔
171
  }: z.ZodDiscriminatedUnion<string, z.ZodDiscriminatedUnionOption<string>[]>,
18✔
172
  { next },
18✔
173
) => {
18✔
174
  return {
18✔
175
    discriminator: { propertyName: discriminator },
18✔
176
    oneOf: options.map(next),
18✔
177
  };
18✔
178
};
18✔
179

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

220
export const depictIntersection: Depicter = (
6✔
221
  { _def: { left, right } }: z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>,
102✔
222
  { next },
102✔
223
) => {
102✔
224
  const children = [left, right].map(next);
102✔
225
  try {
102✔
226
    return tryFlattenIntersection(children);
102✔
227
  } catch {}
102✔
228
  return { allOf: children };
24✔
229
};
24✔
230

231
export const depictOptional: Depicter = (
6✔
232
  schema: z.ZodOptional<z.ZodTypeAny>,
66✔
233
  { next },
66✔
234
) => next(schema.unwrap());
66✔
235

236
export const depictReadonly: Depicter = (
6✔
237
  schema: z.ZodReadonly<z.ZodTypeAny>,
12✔
238
  { next },
12✔
239
) => next(schema.unwrap());
12✔
240

241
/** @since OAS 3.1 nullable replaced with type array having null */
242
export const depictNullable: Depicter = (
6✔
243
  schema: z.ZodNullable<z.ZodTypeAny>,
60✔
244
  { next },
60✔
245
) => {
60✔
246
  const nested = next(schema.unwrap());
60✔
247
  if (isSchemaObject(nested)) {
60✔
248
    nested.type = makeNullableType(nested);
60✔
249
  }
60✔
250
  return nested;
60✔
251
};
60✔
252

253
const getSupportedType = (value: unknown): SchemaObjectType | undefined => {
6✔
254
  const detected = toLower(detectType(value)); // toLower is typed well unlike .toLowerCase()
1,044✔
255
  const isSupported =
1,044✔
256
    detected === "number" ||
1,044✔
257
    detected === "string" ||
1,026✔
258
    detected === "boolean" ||
18✔
259
    detected === "object" ||
18✔
260
    detected === "null" ||
18✔
261
    detected === "array";
12✔
262
  return typeof value === "bigint"
1,044✔
263
    ? "integer"
6✔
264
    : isSupported
1,038✔
265
      ? detected
1,032✔
266
      : undefined;
6✔
267
};
1,044✔
268

269
export const depictEnum: Depicter = (
6✔
270
  schema: z.ZodEnum<[string, ...string[]]> | z.ZodNativeEnum<z.EnumLike>,
24✔
271
) => ({
24✔
272
  type: getSupportedType(Object.values(schema.enum)[0]),
24✔
273
  enum: Object.values(schema.enum),
24✔
274
});
24✔
275

276
export const depictLiteral: Depicter = ({ value }: z.ZodLiteral<unknown>) => ({
6✔
277
  type: getSupportedType(value), // constructor allows z.Primitive only, but ZodLiteral does not have that constraint
1,020✔
278
  const: value,
1,020✔
279
});
1,020✔
280

281
export const depictObject: Depicter = (
6✔
282
  schema: z.ZodObject<z.ZodRawShape>,
2,196✔
283
  { isResponse, next },
2,196✔
284
) => {
2,196✔
285
  const keys = Object.keys(schema.shape);
2,196✔
286
  const isOptionalProp = (prop: z.ZodTypeAny) =>
2,196✔
287
    isResponse && hasCoercion(prop)
3,330✔
288
      ? prop instanceof z.ZodOptional
18✔
289
      : prop.isOptional();
3,312✔
290
  const required = keys.filter((key) => !isOptionalProp(schema.shape[key]));
2,196✔
291
  const result: SchemaObject = { type: "object" };
2,196✔
292
  if (keys.length) {
2,196✔
293
    result.properties = depictObjectProperties(schema, next);
1,998✔
294
  }
1,998✔
295
  if (required.length) {
2,196✔
296
    result.required = required;
1,920✔
297
  }
1,920✔
298
  return result;
2,136✔
299
};
2,136✔
300

301
/**
302
 * @see https://swagger.io/docs/specification/data-models/data-types/
303
 * @since OAS 3.1: using type: "null"
304
 * */
305
export const depictNull: Depicter = () => ({ type: "null" });
6✔
306

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

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

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

357
export const depictBoolean: Depicter = () => ({ type: "boolean" });
6✔
358

359
export const depictBigInt: Depicter = () => ({
6✔
360
  type: "integer",
18✔
361
  format: "bigint",
18✔
362
});
18✔
363

364
const areOptionsLiteral = (
6✔
365
  subject: z.ZodTypeAny[],
12✔
366
): subject is z.ZodLiteral<unknown>[] =>
367
  subject.every((option) => option instanceof z.ZodLiteral);
12✔
368

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

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

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

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

490
/** @since OAS 3.1: exclusive min/max are numbers */
491
export const depictNumber: Depicter = ({
6✔
492
  isInt,
372✔
493
  maxValue,
372✔
494
  minValue,
372✔
495
  _def: { checks },
372✔
496
}: z.ZodNumber) => {
372✔
497
  const minCheck = checks.find((check) => check.kind === "min");
372✔
498
  const minimum =
372✔
499
    minValue === null
372✔
500
      ? isInt
222✔
501
        ? Number.MIN_SAFE_INTEGER
36✔
502
        : -Number.MAX_VALUE
186✔
503
      : minValue;
150✔
504
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
372✔
505
  const maxCheck = checks.find((check) => check.kind === "max");
372✔
506
  const maximum =
372✔
507
    maxValue === null
372✔
508
      ? isInt
330✔
509
        ? Number.MAX_SAFE_INTEGER
144✔
510
        : Number.MAX_VALUE
186✔
511
      : maxValue;
42✔
512
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
372✔
513
  const result: SchemaObject = {
372✔
514
    type: isInt ? "integer" : "number",
372✔
515
    format: isInt ? "int64" : "double",
372✔
516
  };
372✔
517
  if (isMinInclusive) {
372✔
518
    result.minimum = minimum;
294✔
519
  } else {
372✔
520
    result.exclusiveMinimum = minimum;
78✔
521
  }
78✔
522
  if (isMaxInclusive) {
372✔
523
    result.maximum = maximum;
354✔
524
  } else {
372✔
525
    result.exclusiveMaximum = maximum;
18✔
526
  }
18✔
527
  return result;
372✔
528
};
372✔
529

530
export const depictObjectProperties = (
6✔
531
  { shape }: z.ZodObject<z.ZodRawShape>,
2,052✔
532
  next: Parameters<Depicter>[1]["next"],
2,052✔
533
) => map(next, shape);
2,052✔
534

535
const makeSample = (depicted: SchemaObject) => {
6✔
536
  const firstType = (
42✔
537
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
42!
538
  ) as keyof typeof samples;
539
  return samples?.[firstType];
42✔
540
};
42✔
541

542
const makeNullableType = (prev: SchemaObject): SchemaObjectType[] => {
6✔
543
  const current = typeof prev.type === "string" ? [prev.type] : prev.type || [];
138!
544
  if (current.includes("null")) {
138✔
545
    return current;
72✔
546
  }
72✔
547
  return current.concat("null");
66✔
548
};
66✔
549

550
export const depictEffect: Depicter = (
6✔
551
  schema: z.ZodEffects<z.ZodTypeAny>,
186✔
552
  { isResponse, next },
186✔
553
) => {
186✔
554
  const input = next(schema.innerType());
186✔
555
  const { effect } = schema._def;
186✔
556
  if (isResponse && effect.type === "transform" && isSchemaObject(input)) {
186✔
557
    const outputType = tryToTransform(schema, makeSample(input));
42✔
558
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
42✔
559
      return { type: outputType as "number" | "string" | "boolean" };
30✔
560
    } else {
42✔
561
      return next(z.any());
12✔
562
    }
12✔
563
  }
42✔
564
  if (!isResponse && effect.type === "preprocess" && isSchemaObject(input)) {
186✔
565
    const { type: inputType, ...rest } = input;
18✔
566
    return {
18✔
567
      ...rest,
18✔
568
      format: `${rest.format || inputType} (preprocessed)`,
18✔
569
    };
18✔
570
  }
18✔
571
  return input;
126✔
572
};
126✔
573

574
export const depictPipeline: Depicter = (
6✔
575
  { _def }: z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>,
24✔
576
  { isResponse, next },
24✔
577
) => next(_def[isResponse ? "out" : "in"]);
24✔
578

579
export const depictBranded: Depicter = (
6✔
580
  schema: z.ZodBranded<z.ZodTypeAny, string | number | symbol>,
6✔
581
  { next },
6✔
582
) => next(schema.unwrap());
6✔
583

584
export const depictLazy: Depicter = (
6✔
585
  lazy: z.ZodLazy<z.ZodTypeAny>,
54✔
586
  { next, makeRef },
54✔
587
): ReferenceObject => makeRef(lazy, () => next(lazy.schema));
54✔
588

589
export const depictRaw: Depicter = (schema: RawSchema, { next }) =>
6✔
590
  next(schema.unwrap().shape.raw);
12✔
591

592
const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined =>
6✔
593
  examples.length
1,422✔
594
    ? fromPairs(
504✔
595
        zip(
504✔
596
          range(1, examples.length + 1).map((idx) => `example${idx}`),
504✔
597
          map(objOf("value"), examples),
504✔
598
        ),
504✔
599
      )
504✔
600
    : undefined;
918✔
601

602
export const depictExamples = (
6✔
603
  schema: z.ZodTypeAny,
1,134✔
604
  isResponse: boolean,
1,134✔
605
  omitProps: string[] = [],
1,134✔
606
): ExamplesObject | undefined =>
607
  pipe(
1,134✔
608
    getExamples,
1,134✔
609
    map(when((subj) => detectType(subj) === "Object", omit(omitProps))),
1,134✔
610
    enumerateExamples,
1,134✔
611
  )({ schema, variant: isResponse ? "parsed" : "original", validate: true });
1,134✔
612

613
export const depictParamExamples = (
6✔
614
  schema: z.ZodTypeAny,
288✔
615
  param: string,
288✔
616
): ExamplesObject | undefined =>
617
  pipe(
288✔
618
    getExamples,
288✔
619
    filter<FlatObject>(has(param)),
288✔
620
    pluck(param),
288✔
621
    enumerateExamples,
288✔
622
  )({ schema, variant: "original", validate: true });
288✔
623

624
export const extractObjectSchema = (
6✔
625
  subject: IOSchema,
690✔
626
): z.ZodObject<z.ZodRawShape> => {
690✔
627
  if (subject instanceof z.ZodObject) {
690✔
628
    return subject;
570✔
629
  }
570✔
630
  if (subject instanceof z.ZodBranded) {
690✔
631
    return extractObjectSchema(subject.unwrap());
18✔
632
  }
18✔
633
  if (
102✔
634
    subject instanceof z.ZodUnion ||
102✔
635
    subject instanceof z.ZodDiscriminatedUnion
90✔
636
  ) {
690✔
637
    return subject.options
18✔
638
      .map((option) => extractObjectSchema(option))
18✔
639
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
18✔
640
  } else if (subject instanceof z.ZodEffects) {
690✔
641
    return extractObjectSchema(subject._def.schema);
24✔
642
  } else if (subject instanceof z.ZodPipeline) {
84✔
643
    return extractObjectSchema(subject._def.in);
6✔
644
  } // intersection left:
6✔
645
  return extractObjectSchema(subject._def.left).merge(
54✔
646
    extractObjectSchema(subject._def.right),
54✔
647
  );
54✔
648
};
54✔
649

650
export const depictRequestParams = ({
6✔
651
  path,
462✔
652
  method,
462✔
653
  schema,
462✔
654
  inputSources,
462✔
655
  makeRef,
462✔
656
  composition,
462✔
657
  brandHandling,
462✔
658
  description = `${method.toUpperCase()} ${path} Parameter`,
462✔
659
}: ReqResHandlingProps<IOSchema> & {
462✔
660
  inputSources: InputSource[];
661
}) => {
462✔
662
  const { shape } = extractObjectSchema(schema);
462✔
663
  const pathParams = getRoutePathParams(path);
462✔
664
  const isQueryEnabled = inputSources.includes("query");
462✔
665
  const areParamsEnabled = inputSources.includes("params");
462✔
666
  const areHeadersEnabled = inputSources.includes("headers");
462✔
667
  const isPathParam = (name: string) =>
462✔
668
    areParamsEnabled && pathParams.includes(name);
738✔
669
  const isHeaderParam = (name: string) =>
462✔
670
    areHeadersEnabled && isCustomHeader(name);
678✔
671

672
  const parameters = Object.keys(shape)
462✔
673
    .map<{ name: string; location?: ParameterLocation }>((name) => ({
462✔
674
      name,
738✔
675
      location: isPathParam(name)
738✔
676
        ? "path"
60✔
677
        : isHeaderParam(name)
678✔
678
          ? "header"
24✔
679
          : isQueryEnabled
654✔
680
            ? "query"
198✔
681
            : undefined,
456✔
682
    }))
462✔
683
    .filter(
462✔
684
      (parameter): parameter is Required<typeof parameter> =>
462✔
685
        parameter.location !== undefined,
738✔
686
    );
462✔
687

688
  return parameters.map<ParameterObject>(({ name, location }) => {
462✔
689
    const depicted = walkSchema(shape[name], {
282✔
690
      rules: { ...brandHandling, ...depicters },
282✔
691
      onEach,
282✔
692
      onMissing,
282✔
693
      ctx: { isResponse: false, makeRef, path, method },
282✔
694
    });
282✔
695
    const result =
282✔
696
      composition === "components"
282✔
697
        ? makeRef(shape[name], depicted, makeCleanId(description, name))
36✔
698
        : depicted;
246✔
699
    return {
282✔
700
      name,
282✔
701
      in: location,
282✔
702
      required: !shape[name].isOptional(),
282✔
703
      description: depicted.description || description,
282✔
704
      schema: result,
282✔
705
      examples: depictParamExamples(schema, name),
282✔
706
    };
282✔
707
  });
462✔
708
};
462✔
709

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

748
export const onEach: SchemaHandler<
6✔
749
  SchemaObject | ReferenceObject,
750
  OpenAPIContext,
751
  "each"
752
> = (schema: z.ZodTypeAny, { isResponse, prev }) => {
6✔
753
  if (isReferenceObject(prev)) return {};
5,820✔
754
  const { description } = schema;
5,784✔
755
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,784✔
756
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,784✔
757
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,820✔
758
  const isActuallyNullable =
5,820✔
759
    !shouldAvoidParsing &&
5,820✔
760
    hasTypePropertyInDepiction &&
5,784✔
761
    !isResponseHavingCoercion &&
5,610✔
762
    schema.isNullable();
5,580✔
763
  const result: SchemaObject = {};
5,820✔
764
  if (description) result.description = description;
5,820✔
765
  if (isActuallyNullable) result.type = makeNullableType(prev);
5,820✔
766
  if (!shouldAvoidParsing) {
5,784✔
767
    const examples = getExamples({
5,784✔
768
      schema,
5,784✔
769
      variant: isResponse ? "parsed" : "original",
5,784✔
770
      validate: true,
5,784✔
771
    });
5,784✔
772
    if (examples.length) result.examples = examples.slice();
5,784✔
773
  }
5,784✔
774
  return result;
5,784✔
775
};
5,784✔
776

777
export const onMissing: SchemaHandler<
6✔
778
  SchemaObject | ReferenceObject,
779
  OpenAPIContext,
780
  "last"
781
> = (schema: z.ZodTypeAny, ctx) =>
6✔
782
  assert.fail(
60✔
783
    new DocumentationError({
60✔
784
      message: `Zod type ${schema.constructor.name} is unsupported.`,
60✔
785
      ...ctx,
60✔
786
    }),
60✔
787
  );
60✔
788

789
export const excludeParamsFromDepiction = (
6✔
790
  depicted: SchemaObject | ReferenceObject,
276✔
791
  names: string[],
276✔
792
): SchemaObject | ReferenceObject => {
276✔
793
  if (isReferenceObject(depicted)) {
276✔
794
    return depicted;
6✔
795
  }
6✔
796
  const copy = { ...depicted };
270✔
797
  if (copy.properties) {
276✔
798
    copy.properties = omit(names, copy.properties);
204✔
799
  }
204✔
800
  if (copy.examples) {
276✔
801
    copy.examples = copy.examples.map((entry) => omit(names, entry));
30✔
802
  }
30✔
803
  if (copy.required) {
276✔
804
    copy.required = copy.required.filter((name) => !names.includes(name));
204✔
805
  }
204✔
806
  if (copy.allOf) {
276✔
807
    copy.allOf = copy.allOf.map((entry) =>
6✔
808
      excludeParamsFromDepiction(entry, names),
12✔
809
    );
6✔
810
  }
6✔
811
  if (copy.oneOf) {
276✔
812
    copy.oneOf = copy.oneOf.map((entry) =>
18✔
813
      excludeParamsFromDepiction(entry, names),
36✔
814
    );
18✔
815
  }
18✔
816
  return copy;
270✔
817
};
270✔
818

819
export const excludeExamplesFromDepiction = (
6✔
820
  depicted: SchemaObject | ReferenceObject,
1,128✔
821
): SchemaObject | ReferenceObject =>
822
  isReferenceObject(depicted) ? depicted : omit(["examples"], depicted);
1,128!
823

824
export const depictResponse = ({
6✔
825
  method,
924✔
826
  path,
924✔
827
  schema,
924✔
828
  mimeTypes,
924✔
829
  variant,
924✔
830
  makeRef,
924✔
831
  composition,
924✔
832
  hasMultipleStatusCodes,
924✔
833
  statusCode,
924✔
834
  brandHandling,
924✔
835
  description = `${method.toUpperCase()} ${path} ${ucFirst(variant)} response ${
924✔
836
    hasMultipleStatusCodes ? statusCode : ""
924✔
837
  }`.trim(),
924✔
838
}: ReqResHandlingProps<z.ZodTypeAny> & {
924✔
839
  mimeTypes: ReadonlyArray<string>;
840
  variant: ResponseVariant;
841
  statusCode: number;
842
  hasMultipleStatusCodes: boolean;
843
}): ResponseObject => {
924✔
844
  const depictedSchema = excludeExamplesFromDepiction(
924✔
845
    walkSchema(schema, {
924✔
846
      rules: { ...brandHandling, ...depicters },
924✔
847
      onEach,
924✔
848
      onMissing,
924✔
849
      ctx: { isResponse: true, makeRef, path, method },
924✔
850
    }),
924✔
851
  );
924✔
852
  const media: MediaTypeObject = {
924✔
853
    schema:
924✔
854
      composition === "components"
924✔
855
        ? makeRef(schema, depictedSchema, makeCleanId(description))
138✔
856
        : depictedSchema,
786✔
857
    examples: depictExamples(schema, true),
924✔
858
  };
924✔
859
  return { description, content: fromPairs(xprod(mimeTypes, [media])) };
924✔
860
};
924✔
861

862
type SecurityHelper<K extends Security["type"]> = (
863
  security: Extract<Security, { type: K }>,
864
  inputSources?: InputSource[],
865
) => SecuritySchemeObject;
866

867
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
6✔
868
  type: "http",
6✔
869
  scheme: "basic",
6✔
870
});
6✔
871
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
6✔
872
  format: bearerFormat,
24✔
873
}) => {
24✔
874
  const result: SecuritySchemeObject = {
24✔
875
    type: "http",
24✔
876
    scheme: "bearer",
24✔
877
  };
24✔
878
  if (bearerFormat) {
24✔
879
    result.bearerFormat = bearerFormat;
6✔
880
  }
6✔
881
  return result;
24✔
882
};
24✔
883
const depictInputSecurity: SecurityHelper<"input"> = (
6✔
884
  { name },
36✔
885
  inputSources,
36✔
886
) => {
36✔
887
  const result: SecuritySchemeObject = {
36✔
888
    type: "apiKey",
36✔
889
    in: "query",
36✔
890
    name,
36✔
891
  };
36✔
892
  if (inputSources?.includes("body")) {
36✔
893
    if (inputSources?.includes("query")) {
24✔
894
      result["x-in-alternative"] = "body";
6✔
895
      result.description = `${name} CAN also be supplied within the request body`;
6✔
896
    } else {
18✔
897
      result["x-in-actual"] = "body";
18✔
898
      result.description = `${name} MUST be supplied within the request body instead of query`;
18✔
899
    }
18✔
900
  }
24✔
901
  return result;
36✔
902
};
36✔
903
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
6✔
904
  type: "apiKey",
18✔
905
  in: "header",
18✔
906
  name,
18✔
907
});
18✔
908
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
6✔
909
  type: "apiKey",
6✔
910
  in: "cookie",
6✔
911
  name,
6✔
912
});
6✔
913
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
6✔
914
  url: openIdConnectUrl,
6✔
915
}) => ({
6✔
916
  type: "openIdConnect",
6✔
917
  openIdConnectUrl,
6✔
918
});
6✔
919
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
6✔
920
  type: "oauth2",
30✔
921
  flows: map(
30✔
922
    (flow): OAuthFlowObject => ({ ...flow, scopes: flow.scopes || {} }),
30✔
923
    reject(isNil, flows) as Required<typeof flows>,
30✔
924
  ),
30✔
925
});
30✔
926

927
export const depictSecurity = (
6✔
928
  container: LogicalContainer<Security>,
426✔
929
  inputSources?: InputSource[],
426✔
930
): LogicalContainer<SecuritySchemeObject> => {
426✔
931
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
426✔
932
    basic: depictBasicSecurity,
426✔
933
    bearer: depictBearerSecurity,
426✔
934
    input: depictInputSecurity,
426✔
935
    header: depictHeaderSecurity,
426✔
936
    cookie: depictCookieSecurity,
426✔
937
    openid: depictOpenIdSecurity,
426✔
938
    oauth2: depictOAuth2Security,
426✔
939
  };
426✔
940
  return mapLogicalContainer(container, (security) =>
426✔
941
    (methods[security.type] as SecurityHelper<typeof security.type>)(
126✔
942
      security,
126✔
943
      inputSources,
126✔
944
    ),
126✔
945
  );
426✔
946
};
426✔
947

948
export const depictSecurityRefs = (
6✔
949
  container: LogicalContainer<{ name: string; scopes: string[] }>,
804✔
950
): SecurityRequirementObject[] => {
804✔
951
  if ("or" in container) {
804✔
952
    return container.or.map(
414✔
953
      (entry): SecurityRequirementObject =>
414✔
954
        "and" in entry
108✔
955
          ? mergeAll(map(({ name, scopes }) => objOf(name, scopes), entry.and))
42✔
956
          : { [entry.name]: entry.scopes },
66✔
957
    );
414✔
958
  }
414✔
959
  if ("and" in container) {
426✔
960
    return depictSecurityRefs(andToOr(container));
384✔
961
  }
384✔
962
  return depictSecurityRefs({ or: [container] });
6✔
963
};
6✔
964

965
export const depictBody = ({
6✔
966
  method,
258✔
967
  path,
258✔
968
  schema,
258✔
969
  mimeTypes,
258✔
970
  makeRef,
258✔
971
  composition,
258✔
972
  brandHandling,
258✔
973
  paramNames,
258✔
974
  description = `${method.toUpperCase()} ${path} Request body`,
258✔
975
}: ReqResHandlingProps<IOSchema> & {
258✔
976
  mimeTypes: ReadonlyArray<string>;
977
  paramNames: string[];
978
}): RequestBodyObject => {
258✔
979
  const bodyDepiction = excludeExamplesFromDepiction(
258✔
980
    excludeParamsFromDepiction(
258✔
981
      walkSchema(schema, {
258✔
982
        rules: { ...brandHandling, ...depicters },
258✔
983
        onEach,
258✔
984
        onMissing,
258✔
985
        ctx: { isResponse: false, makeRef, path, method },
258✔
986
      }),
258✔
987
      paramNames,
258✔
988
    ),
258✔
989
  );
258✔
990
  const media: MediaTypeObject = {
258✔
991
    schema:
258✔
992
      composition === "components"
258✔
993
        ? makeRef(schema, bodyDepiction, makeCleanId(description))
30✔
994
        : bodyDepiction,
168✔
995
    examples: depictExamples(schema, false, paramNames),
258✔
996
  };
258✔
997
  return { description, content: fromPairs(xprod(mimeTypes, [media])) };
258✔
998
};
258✔
999

1000
export const depictTags = <TAG extends string>(
6✔
1001
  tags: TagsConfig<TAG>,
24✔
1002
): TagObject[] =>
1003
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
1004
    const def = tags[tag];
48✔
1005
    const result: TagObject = {
48✔
1006
      name: tag,
48✔
1007
      description: typeof def === "string" ? def : def.description,
48✔
1008
    };
48✔
1009
    if (typeof def === "object" && def.url) {
48✔
1010
      result.externalDocs = { url: def.url };
6✔
1011
    }
6✔
1012
    return result;
48✔
1013
  });
48✔
1014

1015
export const ensureShortDescription = (description: string) =>
6✔
1016
  description.length <= shortDescriptionLimit
126✔
1017
    ? description
108✔
1018
    : description.slice(0, shortDescriptionLimit - 1) + "…";
18✔
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