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

RobinTail / express-zod-api / 11774601763

11 Nov 2024 08:11AM CUT coverage: 100.0%. Remained the same
11774601763

Pull #2163

github

web-flow
Merge f0bd6d2f1 into f628a2af1
Pull Request #2163: Bump @arethetypeswrong/cli from 0.16.4 to 0.17.0

1148 of 1168 branches covered (98.29%)

3900 of 3900 relevant lines covered (100.0%)

381.94 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 || []);
102!
209
  if (left.examples || right.examples) {
102✔
210
    flat.examples = combinations(
36✔
211
      left.examples || [],
36!
212
      right.examples || [],
36!
213
      ([a, b]) => mergeDeepRight(a, b),
36✔
214
    );
36✔
215
  }
36✔
216
  return flat;
78✔
217
};
78✔
218

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

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

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

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

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

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

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

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

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

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

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

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

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

352
export const depictBigInt: Depicter = () => ({
6✔
353
  type: "integer",
18✔
354
  format: "bigint",
18✔
355
});
18✔
356

357
const areOptionsLiteral = (
6✔
358
  subject: z.ZodTypeAny[],
12✔
359
): subject is z.ZodLiteral<unknown>[] =>
360
  subject.every((option) => option instanceof z.ZodLiteral);
12✔
361

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

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

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

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

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

507
export const depictObjectProperties = (
6✔
508
  { shape }: z.ZodObject<z.ZodRawShape>,
2,052✔
509
  next: Parameters<Depicter>[1]["next"],
2,052✔
510
) => map(next, shape);
2,052✔
511

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

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

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

549
export const depictPipeline: Depicter = (
6✔
550
  { _def }: z.ZodPipeline<z.ZodTypeAny, z.ZodTypeAny>,
24✔
551
  { isResponse, next },
24✔
552
) => next(_def[isResponse ? "out" : "in"]);
24✔
553

554
export const depictBranded: Depicter = (
6✔
555
  schema: z.ZodBranded<z.ZodTypeAny, string | number | symbol>,
6✔
556
  { next },
6✔
557
) => next(schema.unwrap());
6✔
558

559
export const depictLazy: Depicter = (
6✔
560
  lazy: z.ZodLazy<z.ZodTypeAny>,
54✔
561
  { next, makeRef },
54✔
562
): ReferenceObject => makeRef(lazy, () => next(lazy.schema));
54✔
563

564
export const depictRaw: Depicter = (schema: RawSchema, { next }) =>
6✔
565
  next(schema.unwrap().shape.raw);
12✔
566

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

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

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

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

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

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

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

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

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

749
export const onMissing: SchemaHandler<
6✔
750
  SchemaObject | ReferenceObject,
751
  OpenAPIContext,
752
  "last"
753
> = (schema: z.ZodTypeAny, ctx) =>
6✔
754
  assert.fail(
60✔
755
    new DocumentationError({
60✔
756
      message: `Zod type ${schema.constructor.name} is unsupported.`,
60✔
757
      ...ctx,
60✔
758
    }),
60✔
759
  );
60✔
760

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

785
export const excludeExamplesFromDepiction = (
6✔
786
  depicted: SchemaObject | ReferenceObject,
1,128✔
787
): SchemaObject | ReferenceObject =>
788
  isReferenceObject(depicted) ? depicted : omit(["examples"], depicted);
1,128!
789

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

828
type SecurityHelper<K extends Security["type"]> = (
829
  security: Extract<Security, { type: K }>,
830
  inputSources?: InputSource[],
831
) => SecuritySchemeObject;
832

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

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

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

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

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

976
export const ensureShortDescription = (description: string) =>
6✔
977
  description.length <= shortDescriptionLimit
126✔
978
    ? description
108✔
979
    : 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