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

RobinTail / express-zod-api / 11898214981

18 Nov 2024 05:56PM UTC coverage: 100.0%. Remained the same
11898214981

Pull #2189

github

web-flow
Merge 433e74079 into f643b045a
Pull Request #2189: Bump the typescript-eslint group with 2 updates

1162 of 1182 branches covered (98.31%)

3892 of 3892 relevant lines covered (100.0%)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

178
const propsMerger = (a: unknown, b: unknown) => {
6✔
179
  if (Array.isArray(a) && Array.isArray(b)) return concat(a, b);
24✔
180
  if (a === b) return b;
24✔
181
  throw new Error("Can not flatten properties");
6✔
182
};
6✔
183

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

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

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

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

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

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

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

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

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

293
/**
294
 * @see https://swagger.io/docs/specification/data-models/data-types/
295
 * @since OAS 3.1: using type: "null"
296
 * */
297
export const depictNull: Depicter = () => ({ type: "null" });
6✔
298

299
export const depictDateIn: Depicter = ({}: DateInSchema, ctx) => {
6✔
300
  if (ctx.isResponse) {
36✔
301
    throw new DocumentationError({
6✔
302
      message: "Please use ez.dateOut() for output.",
6✔
303
      ...ctx,
6✔
304
    });
6✔
305
  }
6✔
306
  return {
30✔
307
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
30✔
308
    type: "string",
30✔
309
    format: "date-time",
30✔
310
    pattern: /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?)?Z?$/.source,
30✔
311
    externalDocs: {
30✔
312
      url: isoDateDocumentationUrl,
30✔
313
    },
30✔
314
  };
30✔
315
};
30✔
316

317
export const depictDateOut: Depicter = ({}: DateOutSchema, ctx) => {
6✔
318
  if (!ctx.isResponse) {
30✔
319
    throw new DocumentationError({
6✔
320
      message: "Please use ez.dateIn() for input.",
6✔
321
      ...ctx,
6✔
322
    });
6✔
323
  }
6✔
324
  return {
24✔
325
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
24✔
326
    type: "string",
24✔
327
    format: "date-time",
24✔
328
    externalDocs: {
24✔
329
      url: isoDateDocumentationUrl,
24✔
330
    },
24✔
331
  };
24✔
332
};
24✔
333

334
/** @throws DocumentationError */
335
export const depictDate: Depicter = ({}: z.ZodDate, ctx) => {
6✔
336
  throw new DocumentationError({
12✔
337
    message: `Using z.date() within ${
12✔
338
      ctx.isResponse ? "output" : "input"
12✔
339
    } schema is forbidden. Please use ez.date${
12✔
340
      ctx.isResponse ? "Out" : "In"
12✔
341
    }() instead. Check out the documentation for details.`,
12✔
342
    ...ctx,
12✔
343
  });
12✔
344
};
12✔
345

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

348
export const depictBigInt: Depicter = () => ({
6✔
349
  type: "integer",
18✔
350
  format: "bigint",
18✔
351
});
18✔
352

353
const areOptionsLiteral = (
6✔
354
  subject: z.ZodTypeAny[],
12✔
355
): subject is z.ZodLiteral<unknown>[] =>
356
  subject.every((option) => option instanceof z.ZodLiteral);
12✔
357

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

396
export const depictArray: Depicter = (
6✔
397
  { _def: { minLength, maxLength }, element }: z.ZodArray<z.ZodTypeAny>,
84✔
398
  { next },
84✔
399
) => {
84✔
400
  const result: SchemaObject = { type: "array", items: next(element) };
84✔
401
  if (minLength) result.minItems = minLength.value;
84✔
402
  if (maxLength) result.maxItems = maxLength.value;
84✔
403
  return result;
84✔
404
};
84✔
405

406
/**
407
 * @since OAS 3.1 using prefixItems for depicting tuples
408
 * @since 17.5.0 added rest handling, fixed tuple type
409
 * */
410
export const depictTuple: Depicter = (
6✔
411
  { items, _def: { rest } }: z.AnyZodTuple,
42✔
412
  { next },
42✔
413
) => ({
42✔
414
  type: "array",
42✔
415
  prefixItems: items.map(next),
42✔
416
  // does not appear to support items:false, so not:{} is a recommended alias
417
  items: rest === null ? { not: {} } : next(rest),
42✔
418
});
42✔
419

420
export const depictString: Depicter = ({
6✔
421
  isEmail,
1,380✔
422
  isURL,
1,380✔
423
  minLength,
1,380✔
424
  maxLength,
1,380✔
425
  isUUID,
1,380✔
426
  isCUID,
1,380✔
427
  isCUID2,
1,380✔
428
  isULID,
1,380✔
429
  isIP,
1,380✔
430
  isEmoji,
1,380✔
431
  isDatetime,
1,380✔
432
  _def: { checks },
1,380✔
433
}: z.ZodString) => {
1,380✔
434
  const regexCheck = checks.find((check) => check.kind === "regex");
1,380✔
435
  const datetimeCheck = checks.find((check) => check.kind === "datetime");
1,380✔
436
  const regex = regexCheck
1,380✔
437
    ? regexCheck.regex
54✔
438
    : datetimeCheck
1,326✔
439
      ? datetimeCheck.offset
18✔
440
        ? new RegExp(
6✔
441
            `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`,
6✔
442
          )
6✔
443
        : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
12✔
444
      : undefined;
1,308✔
445
  const result: SchemaObject = { type: "string" };
1,380✔
446
  const formats: Record<NonNullable<SchemaObject["format"]>, boolean> = {
1,380✔
447
    "date-time": isDatetime,
1,380✔
448
    email: isEmail,
1,380✔
449
    url: isURL,
1,380✔
450
    uuid: isUUID,
1,380✔
451
    cuid: isCUID,
1,380✔
452
    cuid2: isCUID2,
1,380✔
453
    ulid: isULID,
1,380✔
454
    ip: isIP,
1,380✔
455
    emoji: isEmoji,
1,380✔
456
  };
1,380✔
457
  for (const format in formats) {
1,380✔
458
    if (formats[format]) {
11,934✔
459
      result.format = format;
96✔
460
      break;
96✔
461
    }
96✔
462
  }
11,934✔
463
  if (minLength !== null) result.minLength = minLength;
1,380✔
464
  if (maxLength !== null) result.maxLength = maxLength;
1,380✔
465
  if (regex) result.pattern = regex.source;
1,380✔
466
  return result;
1,380✔
467
};
1,380✔
468

469
/** @since OAS 3.1: exclusive min/max are numbers */
470
export const depictNumber: Depicter = ({
6✔
471
  isInt,
372✔
472
  maxValue,
372✔
473
  minValue,
372✔
474
  _def: { checks },
372✔
475
}: z.ZodNumber) => {
372✔
476
  const minCheck = checks.find((check) => check.kind === "min");
372✔
477
  const minimum =
372✔
478
    minValue === null
372✔
479
      ? isInt
222✔
480
        ? Number.MIN_SAFE_INTEGER
36✔
481
        : -Number.MAX_VALUE
186✔
482
      : minValue;
150✔
483
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
372✔
484
  const maxCheck = checks.find((check) => check.kind === "max");
372✔
485
  const maximum =
372✔
486
    maxValue === null
372✔
487
      ? isInt
330✔
488
        ? Number.MAX_SAFE_INTEGER
144✔
489
        : Number.MAX_VALUE
186✔
490
      : maxValue;
42✔
491
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
372✔
492
  const result: SchemaObject = {
372✔
493
    type: isInt ? "integer" : "number",
372✔
494
    format: isInt ? "int64" : "double",
372✔
495
  };
372✔
496
  if (isMinInclusive) result.minimum = minimum;
372✔
497
  else result.exclusiveMinimum = minimum;
78✔
498
  if (isMaxInclusive) result.maximum = maximum;
372✔
499
  else result.exclusiveMaximum = maximum;
18✔
500
  return result;
372✔
501
};
372✔
502

503
export const depictObjectProperties = (
6✔
504
  { shape }: z.ZodObject<z.ZodRawShape>,
2,052✔
505
  next: Parameters<Depicter>[1]["next"],
2,052✔
506
) => map(next, shape);
2,052✔
507

508
const makeSample = (depicted: SchemaObject) => {
6✔
509
  const firstType = (
42✔
510
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
42!
511
  ) as keyof typeof samples;
512
  return samples?.[firstType];
42✔
513
};
42✔
514

515
const makeNullableType = ({
6✔
516
  type,
132✔
517
}: SchemaObject): SchemaObjectType | SchemaObjectType[] => {
132✔
518
  if (type === "null") return type;
132✔
519
  if (typeof type === "string") return [type, "null"];
132✔
520
  return type ? [...new Set(type).add("null")] : "null";
132!
521
};
132✔
522

523
export const depictEffect: Depicter = (
6✔
524
  schema: z.ZodEffects<z.ZodTypeAny>,
186✔
525
  { isResponse, next },
186✔
526
) => {
186✔
527
  const input = next(schema.innerType());
186✔
528
  const { effect } = schema._def;
186✔
529
  if (isResponse && effect.type === "transform" && isSchemaObject(input)) {
186✔
530
    const outputType = tryToTransform(schema, makeSample(input));
42✔
531
    if (outputType && ["number", "string", "boolean"].includes(outputType))
42✔
532
      return { type: outputType as "number" | "string" | "boolean" };
42✔
533
    else return next(z.any());
12✔
534
  }
42✔
535
  if (!isResponse && effect.type === "preprocess" && isSchemaObject(input)) {
186✔
536
    const { type: inputType, ...rest } = input;
18✔
537
    return {
18✔
538
      ...rest,
18✔
539
      format: `${rest.format || inputType} (preprocessed)`,
18✔
540
    };
18✔
541
  }
18✔
542
  return input;
126✔
543
};
126✔
544

545
export const depictPipeline: Depicter = (
6✔
546
  { _def }: z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>,
24✔
547
  { isResponse, next },
24✔
548
) => next(_def[isResponse ? "out" : "in"]);
24✔
549

550
export const depictBranded: Depicter = (
6✔
551
  schema: z.ZodBranded<z.ZodTypeAny, string | number | symbol>,
6✔
552
  { next },
6✔
553
) => next(schema.unwrap());
6✔
554

555
export const depictLazy: Depicter = (
6✔
556
  lazy: z.ZodLazy<z.ZodTypeAny>,
54✔
557
  { next, makeRef },
54✔
558
): ReferenceObject => makeRef(lazy, () => next(lazy.schema));
54✔
559

560
export const depictRaw: Depicter = (schema: RawSchema, { next }) =>
6✔
561
  next(schema.unwrap().shape.raw);
12✔
562

563
const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined =>
6✔
564
  examples.length
1,422✔
565
    ? fromPairs(
504✔
566
        zip(
504✔
567
          range(1, examples.length + 1).map((idx) => `example${idx}`),
504✔
568
          map(objOf("value"), examples),
504✔
569
        ),
504✔
570
      )
504✔
571
    : undefined;
918✔
572

573
export const depictExamples = (
6✔
574
  schema: z.ZodTypeAny,
1,134✔
575
  isResponse: boolean,
1,134✔
576
  omitProps: string[] = [],
1,134✔
577
): ExamplesObject | undefined =>
578
  pipe(
1,134✔
579
    getExamples,
1,134✔
580
    map(when((subj) => detectType(subj) === "Object", omit(omitProps))),
1,134✔
581
    enumerateExamples,
1,134✔
582
  )({ schema, variant: isResponse ? "parsed" : "original", validate: true });
1,134✔
583

584
export const depictParamExamples = (
6✔
585
  schema: z.ZodTypeAny,
288✔
586
  param: string,
288✔
587
): ExamplesObject | undefined =>
588
  pipe(
288✔
589
    getExamples,
288✔
590
    filter<FlatObject>(has(param)),
288✔
591
    pluck(param),
288✔
592
    enumerateExamples,
288✔
593
  )({ schema, variant: "original", validate: true });
288✔
594

595
export const extractObjectSchema = (
6✔
596
  subject: IOSchema,
690✔
597
): z.ZodObject<z.ZodRawShape> => {
690✔
598
  if (subject instanceof z.ZodObject) return subject;
690✔
599
  if (subject instanceof z.ZodBranded)
120✔
600
    return extractObjectSchema(subject.unwrap());
690✔
601
  if (
102✔
602
    subject instanceof z.ZodUnion ||
102✔
603
    subject instanceof z.ZodDiscriminatedUnion
90✔
604
  ) {
690✔
605
    return subject.options
18✔
606
      .map((option) => extractObjectSchema(option))
18✔
607
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
18✔
608
  } else if (subject instanceof z.ZodEffects) {
690✔
609
    return extractObjectSchema(subject._def.schema);
24✔
610
  } else if (subject instanceof z.ZodPipeline) {
84✔
611
    return extractObjectSchema(subject._def.in);
6✔
612
  } // intersection left:
6✔
613
  return extractObjectSchema(subject._def.left).merge(
54✔
614
    extractObjectSchema(subject._def.right),
54✔
615
  );
54✔
616
};
54✔
617

618
export const depictRequestParams = ({
6✔
619
  path,
462✔
620
  method,
462✔
621
  schema,
462✔
622
  inputSources,
462✔
623
  makeRef,
462✔
624
  composition,
462✔
625
  brandHandling,
462✔
626
  description = `${method.toUpperCase()} ${path} Parameter`,
462✔
627
}: ReqResHandlingProps<IOSchema> & {
462✔
628
  inputSources: InputSource[];
629
}) => {
462✔
630
  const { shape } = extractObjectSchema(schema);
462✔
631
  const pathParams = getRoutePathParams(path);
462✔
632
  const isQueryEnabled = inputSources.includes("query");
462✔
633
  const areParamsEnabled = inputSources.includes("params");
462✔
634
  const areHeadersEnabled = inputSources.includes("headers");
462✔
635
  const isPathParam = (name: string) =>
462✔
636
    areParamsEnabled && pathParams.includes(name);
738✔
637
  const isHeaderParam = (name: string) =>
462✔
638
    areHeadersEnabled && isCustomHeader(name);
678✔
639

640
  const parameters = Object.keys(shape)
462✔
641
    .map<{ name: string; location?: ParameterLocation }>((name) => ({
462✔
642
      name,
738✔
643
      location: isPathParam(name)
738✔
644
        ? "path"
60✔
645
        : isHeaderParam(name)
678✔
646
          ? "header"
24✔
647
          : isQueryEnabled
654✔
648
            ? "query"
198✔
649
            : undefined,
456✔
650
    }))
462✔
651
    .filter(
462✔
652
      (parameter): parameter is Required<typeof parameter> =>
462✔
653
        parameter.location !== undefined,
738✔
654
    );
462✔
655

656
  return parameters.map<ParameterObject>(({ name, location }) => {
462✔
657
    const depicted = walkSchema(shape[name], {
282✔
658
      rules: { ...brandHandling, ...depicters },
282✔
659
      onEach,
282✔
660
      onMissing,
282✔
661
      ctx: { isResponse: false, makeRef, path, method },
282✔
662
    });
282✔
663
    const result =
282✔
664
      composition === "components"
282✔
665
        ? makeRef(shape[name], depicted, makeCleanId(description, name))
36✔
666
        : depicted;
246✔
667
    return {
282✔
668
      name,
282✔
669
      in: location,
282✔
670
      required: !shape[name].isOptional(),
282✔
671
      description: depicted.description || description,
282✔
672
      schema: result,
282✔
673
      examples: depictParamExamples(schema, name),
282✔
674
    };
282✔
675
  });
462✔
676
};
462✔
677

678
export const depicters: HandlingRules<
6✔
679
  SchemaObject | ReferenceObject,
680
  OpenAPIContext,
681
  z.ZodFirstPartyTypeKind | ProprietaryBrand
682
> = {
6✔
683
  ZodString: depictString,
6✔
684
  ZodNumber: depictNumber,
6✔
685
  ZodBigInt: depictBigInt,
6✔
686
  ZodBoolean: depictBoolean,
6✔
687
  ZodNull: depictNull,
6✔
688
  ZodArray: depictArray,
6✔
689
  ZodTuple: depictTuple,
6✔
690
  ZodRecord: depictRecord,
6✔
691
  ZodObject: depictObject,
6✔
692
  ZodLiteral: depictLiteral,
6✔
693
  ZodIntersection: depictIntersection,
6✔
694
  ZodUnion: depictUnion,
6✔
695
  ZodAny: depictAny,
6✔
696
  ZodDefault: depictDefault,
6✔
697
  ZodEnum: depictEnum,
6✔
698
  ZodNativeEnum: depictEnum,
6✔
699
  ZodEffects: depictEffect,
6✔
700
  ZodOptional: depictOptional,
6✔
701
  ZodNullable: depictNullable,
6✔
702
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
6✔
703
  ZodBranded: depictBranded,
6✔
704
  ZodDate: depictDate,
6✔
705
  ZodCatch: depictCatch,
6✔
706
  ZodPipeline: depictPipeline,
6✔
707
  ZodLazy: depictLazy,
6✔
708
  ZodReadonly: depictReadonly,
6✔
709
  [ezFileBrand]: depictFile,
6✔
710
  [ezUploadBrand]: depictUpload,
6✔
711
  [ezDateOutBrand]: depictDateOut,
6✔
712
  [ezDateInBrand]: depictDateIn,
6✔
713
  [ezRawBrand]: depictRaw,
6✔
714
};
6✔
715

716
export const onEach: SchemaHandler<
6✔
717
  SchemaObject | ReferenceObject,
718
  OpenAPIContext,
719
  "each"
720
> = (schema: z.ZodTypeAny, { isResponse, prev }) => {
6✔
721
  if (isReferenceObject(prev)) return {};
5,814✔
722
  const { description } = schema;
5,778✔
723
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,778✔
724
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,778✔
725
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,814✔
726
  const isActuallyNullable =
5,814✔
727
    !shouldAvoidParsing &&
5,814✔
728
    hasTypePropertyInDepiction &&
5,778✔
729
    !isResponseHavingCoercion &&
5,604✔
730
    schema.isNullable();
5,574✔
731
  const result: SchemaObject = {};
5,814✔
732
  if (description) result.description = description;
5,814✔
733
  if (isActuallyNullable) result.type = makeNullableType(prev);
5,814✔
734
  if (!shouldAvoidParsing) {
5,778✔
735
    const examples = getExamples({
5,778✔
736
      schema,
5,778✔
737
      variant: isResponse ? "parsed" : "original",
5,778✔
738
      validate: true,
5,778✔
739
    });
5,778✔
740
    if (examples.length) result.examples = examples.slice();
5,778✔
741
  }
5,778✔
742
  return result;
5,778✔
743
};
5,778✔
744

745
export const onMissing: SchemaHandler<
6✔
746
  SchemaObject | ReferenceObject,
747
  OpenAPIContext,
748
  "last"
749
> = (schema: z.ZodTypeAny, ctx) => {
6✔
750
  throw new DocumentationError({
60✔
751
    message: `Zod type ${schema.constructor.name} is unsupported.`,
60✔
752
    ...ctx,
60✔
753
  });
60✔
754
};
60✔
755

756
export const excludeParamsFromDepiction = (
6✔
757
  depicted: SchemaObject | ReferenceObject,
276✔
758
  names: string[],
276✔
759
): SchemaObject | ReferenceObject => {
276✔
760
  if (isReferenceObject(depicted)) return depicted;
276✔
761
  const copy = { ...depicted };
270✔
762
  if (copy.properties) copy.properties = omit(names, copy.properties);
276✔
763
  if (copy.examples)
270✔
764
    copy.examples = copy.examples.map((entry) => omit(names, entry));
276✔
765
  if (copy.required)
270✔
766
    copy.required = copy.required.filter((name) => !names.includes(name));
276✔
767
  if (copy.allOf) {
276✔
768
    copy.allOf = copy.allOf.map((entry) =>
6✔
769
      excludeParamsFromDepiction(entry, names),
12✔
770
    );
6✔
771
  }
6✔
772
  if (copy.oneOf) {
276✔
773
    copy.oneOf = copy.oneOf.map((entry) =>
18✔
774
      excludeParamsFromDepiction(entry, names),
36✔
775
    );
18✔
776
  }
18✔
777
  return copy;
270✔
778
};
270✔
779

780
export const excludeExamplesFromDepiction = (
6✔
781
  depicted: SchemaObject | ReferenceObject,
1,128✔
782
): SchemaObject | ReferenceObject =>
783
  isReferenceObject(depicted) ? depicted : omit(["examples"], depicted);
1,128!
784

785
export const depictResponse = ({
6✔
786
  method,
924✔
787
  path,
924✔
788
  schema,
924✔
789
  mimeTypes,
924✔
790
  variant,
924✔
791
  makeRef,
924✔
792
  composition,
924✔
793
  hasMultipleStatusCodes,
924✔
794
  statusCode,
924✔
795
  brandHandling,
924✔
796
  description = `${method.toUpperCase()} ${path} ${ucFirst(variant)} response ${
924✔
797
    hasMultipleStatusCodes ? statusCode : ""
924✔
798
  }`.trim(),
924✔
799
}: ReqResHandlingProps<z.ZodTypeAny> & {
924✔
800
  mimeTypes: ReadonlyArray<string>;
801
  variant: ResponseVariant;
802
  statusCode: number;
803
  hasMultipleStatusCodes: boolean;
804
}): ResponseObject => {
924✔
805
  const depictedSchema = excludeExamplesFromDepiction(
924✔
806
    walkSchema(schema, {
924✔
807
      rules: { ...brandHandling, ...depicters },
924✔
808
      onEach,
924✔
809
      onMissing,
924✔
810
      ctx: { isResponse: true, makeRef, path, method },
924✔
811
    }),
924✔
812
  );
924✔
813
  const media: MediaTypeObject = {
924✔
814
    schema:
924✔
815
      composition === "components"
924✔
816
        ? makeRef(schema, depictedSchema, makeCleanId(description))
138✔
817
        : depictedSchema,
786✔
818
    examples: depictExamples(schema, true),
924✔
819
  };
924✔
820
  return { description, content: fromPairs(xprod(mimeTypes, [media])) };
924✔
821
};
924✔
822

823
type SecurityHelper<K extends Security["type"]> = (
824
  security: Extract<Security, { type: K }>,
825
  inputSources?: InputSource[],
826
) => SecuritySchemeObject;
827

828
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
6✔
829
  type: "http",
6✔
830
  scheme: "basic",
6✔
831
});
6✔
832
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
6✔
833
  format: bearerFormat,
24✔
834
}) => {
24✔
835
  const result: SecuritySchemeObject = {
24✔
836
    type: "http",
24✔
837
    scheme: "bearer",
24✔
838
  };
24✔
839
  if (bearerFormat) result.bearerFormat = bearerFormat;
24✔
840
  return result;
24✔
841
};
24✔
842
const depictInputSecurity: SecurityHelper<"input"> = (
6✔
843
  { name },
36✔
844
  inputSources,
36✔
845
) => {
36✔
846
  const result: SecuritySchemeObject = {
36✔
847
    type: "apiKey",
36✔
848
    in: "query",
36✔
849
    name,
36✔
850
  };
36✔
851
  if (inputSources?.includes("body")) {
36✔
852
    if (inputSources?.includes("query")) {
24✔
853
      result["x-in-alternative"] = "body";
6✔
854
      result.description = `${name} CAN also be supplied within the request body`;
6✔
855
    } else {
18✔
856
      result["x-in-actual"] = "body";
18✔
857
      result.description = `${name} MUST be supplied within the request body instead of query`;
18✔
858
    }
18✔
859
  }
24✔
860
  return result;
36✔
861
};
36✔
862
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
6✔
863
  type: "apiKey",
18✔
864
  in: "header",
18✔
865
  name,
18✔
866
});
18✔
867
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
6✔
868
  type: "apiKey",
6✔
869
  in: "cookie",
6✔
870
  name,
6✔
871
});
6✔
872
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
6✔
873
  url: openIdConnectUrl,
6✔
874
}) => ({
6✔
875
  type: "openIdConnect",
6✔
876
  openIdConnectUrl,
6✔
877
});
6✔
878
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
6✔
879
  type: "oauth2",
30✔
880
  flows: map(
30✔
881
    (flow): OAuthFlowObject => ({ ...flow, scopes: flow.scopes || {} }),
30✔
882
    reject(isNil, flows) as Required<typeof flows>,
30✔
883
  ),
30✔
884
});
30✔
885

886
export const depictSecurity = (
6✔
887
  container: LogicalContainer<Security>,
426✔
888
  inputSources?: InputSource[],
426✔
889
): LogicalContainer<SecuritySchemeObject> => {
426✔
890
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
426✔
891
    basic: depictBasicSecurity,
426✔
892
    bearer: depictBearerSecurity,
426✔
893
    input: depictInputSecurity,
426✔
894
    header: depictHeaderSecurity,
426✔
895
    cookie: depictCookieSecurity,
426✔
896
    openid: depictOpenIdSecurity,
426✔
897
    oauth2: depictOAuth2Security,
426✔
898
  };
426✔
899
  return mapLogicalContainer(container, (security) =>
426✔
900
    (methods[security.type] as SecurityHelper<typeof security.type>)(
126✔
901
      security,
126✔
902
      inputSources,
126✔
903
    ),
126✔
904
  );
426✔
905
};
426✔
906

907
export const depictSecurityRefs = (
6✔
908
  container: LogicalContainer<{ name: string; scopes: string[] }>,
804✔
909
): SecurityRequirementObject[] => {
804✔
910
  if ("or" in container) {
804✔
911
    return container.or.map(
414✔
912
      (entry): SecurityRequirementObject =>
414✔
913
        "and" in entry
108✔
914
          ? mergeAll(map(({ name, scopes }) => objOf(name, scopes), entry.and))
42✔
915
          : { [entry.name]: entry.scopes },
66✔
916
    );
414✔
917
  }
414✔
918
  if ("and" in container) return depictSecurityRefs(andToOr(container));
426✔
919
  return depictSecurityRefs({ or: [container] });
6✔
920
};
6✔
921

922
export const depictBody = ({
6✔
923
  method,
258✔
924
  path,
258✔
925
  schema,
258✔
926
  mimeTypes,
258✔
927
  makeRef,
258✔
928
  composition,
258✔
929
  brandHandling,
258✔
930
  paramNames,
258✔
931
  description = `${method.toUpperCase()} ${path} Request body`,
258✔
932
}: ReqResHandlingProps<IOSchema> & {
258✔
933
  mimeTypes: ReadonlyArray<string>;
934
  paramNames: string[];
935
}): RequestBodyObject => {
258✔
936
  const bodyDepiction = excludeExamplesFromDepiction(
258✔
937
    excludeParamsFromDepiction(
258✔
938
      walkSchema(schema, {
258✔
939
        rules: { ...brandHandling, ...depicters },
258✔
940
        onEach,
258✔
941
        onMissing,
258✔
942
        ctx: { isResponse: false, makeRef, path, method },
258✔
943
      }),
258✔
944
      paramNames,
258✔
945
    ),
258✔
946
  );
258✔
947
  const media: MediaTypeObject = {
258✔
948
    schema:
258✔
949
      composition === "components"
258✔
950
        ? makeRef(schema, bodyDepiction, makeCleanId(description))
30✔
951
        : bodyDepiction,
168✔
952
    examples: depictExamples(schema, false, paramNames),
258✔
953
  };
258✔
954
  return { description, content: fromPairs(xprod(mimeTypes, [media])) };
258✔
955
};
258✔
956

957
export const depictTags = <TAG extends string>(
6✔
958
  tags: TagsConfig<TAG>,
24✔
959
): TagObject[] =>
960
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
961
    const def = tags[tag];
48✔
962
    const result: TagObject = {
48✔
963
      name: tag,
48✔
964
      description: typeof def === "string" ? def : def.description,
48✔
965
    };
48✔
966
    if (typeof def === "object" && def.url)
48✔
967
      result.externalDocs = { url: def.url };
48✔
968
    return result;
48✔
969
  });
48✔
970

971
export const ensureShortDescription = (description: string) =>
6✔
972
  description.length <= shortDescriptionLimit
126✔
973
    ? description
108✔
974
    : 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