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

RobinTail / express-zod-api / 5894877484

17 Aug 2023 07:01PM CUT coverage: 100.0%. Remained the same
5894877484

Pull #1094

github

web-flow
Merge 897258952 into 6f7c035ba
Pull Request #1094: Bump @typescript-eslint/parser from 6.0.0 to 6.4.0

582 of 609 branches covered (95.57%)

1159 of 1159 relevant lines covered (100.0%)

493.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.93
/src/documentation-helpers.ts
1
import {
2
  ContentObject,
3
  ExampleObject,
4
  ExamplesObject,
5
  MediaTypeObject,
6
  OAuthFlowsObject,
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/oas30";
168✔
19
import { omit } from "ramda";
168✔
20
import { z } from "zod";
168✔
21
import {
22
  getExamples,
23
  getRoutePathParams,
24
  hasCoercion,
25
  hasTopLevelTransformingEffect,
26
  makeCleanId,
27
  routePathParamsRegex,
28
  tryToTransform,
29
} from "./common-helpers";
168✔
30
import { InputSource, TagsConfig } from "./config-type";
31
import { ZodDateIn, isoDateRegex } from "./date-in-schema";
168✔
32
import { ZodDateOut } from "./date-out-schema";
33
import { AbstractEndpoint } from "./endpoint";
34
import { DocumentationError } from "./errors";
168✔
35
import { ZodFile } from "./file-schema";
36
import { IOSchema } from "./io-schema";
37
import {
38
  LogicalContainer,
39
  andToOr,
40
  mapLogicalContainer,
41
} from "./logical-container";
168✔
42
import { copyMeta } from "./metadata";
168✔
43
import { Method } from "./method";
44
import {
45
  HandlingRules,
46
  HandlingVariant,
47
  SchemaHandler,
48
  walkSchema,
49
} from "./schema-walker";
168✔
50
import { Security } from "./security";
51
import { ZodUpload } from "./upload-schema";
52

53
type MediaExamples = Pick<MediaTypeObject, "examples">;
54

55
export interface OpenAPIContext {
56
  isResponse: boolean;
57
  serializer: (schema: z.ZodTypeAny) => string;
58
  getRef: (name: string) => ReferenceObject | undefined;
59
  makeRef: (
60
    name: string,
61
    schema: SchemaObject | ReferenceObject,
62
  ) => ReferenceObject;
63
  path: string;
64
  method: Method;
65
}
66

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

72
interface ReqResDepictHelperCommonProps
73
  extends Pick<
74
    OpenAPIContext,
75
    "serializer" | "getRef" | "makeRef" | "path" | "method"
76
  > {
77
  endpoint: AbstractEndpoint;
78
  composition: "inline" | "components";
79
  clue?: string;
80
}
81

82
const shortDescriptionLimit = 50;
168✔
83
const isoDateDocumentationUrl =
84
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
168✔
85

86
const samples: Record<
87
  Exclude<NonNullable<SchemaObjectType>, Array<any>>,
88
  any
89
> = {
168✔
90
  integer: 0,
91
  number: 0,
92
  string: "",
93
  boolean: false,
94
  object: {},
95
  null: null,
96
  array: [],
97
};
98

99
/* eslint-disable @typescript-eslint/no-use-before-define */
100

101
export const reformatParamsInPath = (path: string) =>
384✔
102
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
384✔
103

104
export const depictDefault: Depicter<z.ZodDefault<z.ZodTypeAny>> = ({
168✔
105
  schema: {
106
    _def: { innerType, defaultValue },
107
  },
108
  next,
109
}) => ({
24✔
110
  ...next({ schema: innerType }),
111
  default: defaultValue(),
112
});
113

114
export const depictCatch: Depicter<z.ZodCatch<z.ZodTypeAny>> = ({
168✔
115
  schema: {
116
    _def: { innerType },
117
  },
118
  next,
119
}) => next({ schema: innerType });
8✔
120

121
export const depictAny: Depicter<z.ZodAny> = () => ({
168✔
122
  format: "any",
123
});
124

125
export const depictUpload: Depicter<ZodUpload> = (ctx) => {
168✔
126
  if (ctx.isResponse) {
32✔
127
    throw new DocumentationError({
8✔
128
      message: "Please use z.upload() only for input.",
129
      ...ctx,
130
    });
131
  }
132
  return {
24✔
133
    type: "string",
134
    format: "binary",
135
  };
136
};
137

138
export const depictFile: Depicter<ZodFile> = ({
168✔
139
  schema: { isBinary, isBase64 },
140
  ...ctx
141
}) => {
142
  if (!ctx.isResponse) {
48✔
143
    throw new DocumentationError({
8✔
144
      message: "Please use z.file() only within ResultHandler.",
145
      ...ctx,
146
    });
147
  }
148
  return {
40✔
149
    type: "string",
150
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
56✔
151
  };
152
};
153

154
export const depictUnion: Depicter<
8✔
155
  z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
156
> = ({ schema: { options }, next }) => ({
168✔
157
  oneOf: options.map((option) => next({ schema: option })),
144✔
158
});
159

160
export const depictDiscriminatedUnion: Depicter<
8✔
161
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
162
> = ({ schema: { options, discriminator }, next }) => {
168✔
163
  return {
24✔
164
    discriminator: { propertyName: discriminator },
165
    oneOf: Array.from(options.values()).map((option) =>
166
      next({ schema: option }),
48✔
167
    ),
168
  };
169
};
170

171
export const depictIntersection: Depicter<
8✔
172
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
173
> = ({
168✔
174
  schema: {
175
    _def: { left, right },
176
  },
177
  next,
178
}) => ({
72✔
179
  allOf: [left, right].map((entry) => next({ schema: entry })),
144✔
180
});
181

182
export const depictOptional: Depicter<z.ZodOptional<any>> = ({
168✔
183
  schema,
184
  next,
185
}) => next({ schema: schema.unwrap() });
88✔
186

187
export const depictNullable: Depicter<z.ZodNullable<any>> = ({
168✔
188
  schema,
189
  next,
190
}) => ({
48✔
191
  nullable: true,
192
  ...next({ schema: schema.unwrap() }),
193
});
194

195
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
168✔
196
  schema,
197
}) => ({
32✔
198
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
199
  enum: Object.values(schema.enum),
200
});
201

202
export const depictLiteral: Depicter<z.ZodLiteral<any>> = ({
168✔
203
  schema: { value },
204
}) => ({
872✔
205
  type: typeof value as "string" | "number" | "boolean",
206
  enum: [value],
207
});
208

209
export const depictObject: Depicter<z.AnyZodObject> = ({
168✔
210
  schema,
211
  isResponse,
212
  ...rest
213
}) => {
214
  const required = Object.keys(schema.shape).filter((key) => {
2,056✔
215
    const prop = schema.shape[key];
3,272✔
216
    const isOptional =
217
      isResponse && hasCoercion(prop)
3,272✔
218
        ? prop instanceof z.ZodOptional
219
        : prop.isOptional();
220
    return !isOptional;
3,272✔
221
  });
222
  return {
2,056✔
223
    type: "object",
224
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
225
    ...(required.length ? { required } : {}),
2,000✔
226
  };
227
};
228

229
/**
230
 * @see https://swagger.io/docs/specification/data-models/data-types/
231
 * @todo use type:"null" for OpenAPI 3.1
232
 * */
233
export const depictNull: Depicter<z.ZodNull> = () => ({
168✔
234
  type: "string",
235
  nullable: true,
236
  format: "null",
237
});
238

239
export const depictDateIn: Depicter<ZodDateIn> = (ctx) => {
168✔
240
  if (ctx.isResponse) {
40✔
241
    throw new DocumentationError({
8✔
242
      message: "Please use z.dateOut() for output.",
243
      ...ctx,
244
    });
245
  }
246
  return {
32✔
247
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
248
    type: "string",
249
    format: "date-time",
250
    pattern: isoDateRegex.source,
251
    externalDocs: {
252
      url: isoDateDocumentationUrl,
253
    },
254
  };
255
};
256

257
export const depictDateOut: Depicter<ZodDateOut> = (ctx) => {
168✔
258
  if (!ctx.isResponse) {
40✔
259
    throw new DocumentationError({
8✔
260
      message: "Please use z.dateIn() for input.",
261
      ...ctx,
262
    });
263
  }
264
  return {
32✔
265
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
266
    type: "string",
267
    format: "date-time",
268
    externalDocs: {
269
      url: isoDateDocumentationUrl,
270
    },
271
  };
272
};
273

274
/** @throws DocumentationError */
275
export const depictDate: Depicter<z.ZodDate> = (ctx) => {
168✔
276
  throw new DocumentationError({
16✔
277
    message: `Using z.date() within ${
278
      ctx.isResponse ? "output" : "input"
16✔
279
    } schema is forbidden. Please use z.date${
280
      ctx.isResponse ? "Out" : "In"
16✔
281
    }() instead. Check out the documentation for details.`,
282
    ...ctx,
283
  });
284
};
285

286
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
240✔
287
  type: "boolean",
288
});
289

290
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
168✔
291
  type: "integer",
292
  format: "bigint",
293
});
294

295
export const depictRecord: Depicter<z.ZodRecord<z.ZodTypeAny>> = ({
168✔
296
  schema: { keySchema, valueSchema },
297
  ...rest
298
}) => {
299
  if (keySchema instanceof z.ZodEnum || keySchema instanceof z.ZodNativeEnum) {
112✔
300
    const keys = Object.values(keySchema.enum) as string[];
16✔
301
    const shape = keys.reduce(
16✔
302
      (carry, key) => ({
32✔
303
        ...carry,
304
        [key]: valueSchema,
305
      }),
306
      {} as z.ZodRawShape,
307
    );
308
    return {
16✔
309
      type: "object",
310
      properties: depictObjectProperties({
311
        schema: z.object(shape),
312
        ...rest,
313
      }),
314
      ...(keys.length ? { required: keys } : {}),
16!
315
    };
316
  }
317
  if (keySchema instanceof z.ZodLiteral) {
96✔
318
    return {
16✔
319
      type: "object",
320
      properties: depictObjectProperties({
321
        schema: z.object({
322
          [keySchema.value]: valueSchema,
323
        }),
324
        ...rest,
325
      }),
326
      required: [keySchema.value],
327
    };
328
  }
329
  if (keySchema instanceof z.ZodUnion) {
80✔
330
    const areOptionsLiteral = keySchema.options.reduce(
16✔
331
      (carry: boolean, option: z.ZodTypeAny) =>
332
        carry && option instanceof z.ZodLiteral,
32✔
333
      true,
334
    );
335
    if (areOptionsLiteral) {
16✔
336
      const shape = keySchema.options.reduce(
16✔
337
        (carry: z.ZodRawShape, option: z.ZodLiteral<any>) => ({
32✔
338
          ...carry,
339
          [option.value]: valueSchema,
340
        }),
341
        {} as z.ZodRawShape,
342
      );
343
      return {
16✔
344
        type: "object",
345
        properties: depictObjectProperties({
346
          schema: z.object(shape),
347
          ...rest,
348
        }),
349
        required: keySchema.options.map(
350
          (option: z.ZodLiteral<any>) => option.value,
32✔
351
        ),
352
      };
353
    }
354
  }
355
  return {
64✔
356
    type: "object",
357
    additionalProperties: rest.next({ schema: valueSchema }),
358
  };
359
};
360

361
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
168✔
362
  schema: { _def: def, element },
363
  next,
364
}) => ({
104✔
365
  type: "array",
366
  items: next({ schema: element }),
367
  ...(def.minLength !== null && { minItems: def.minLength.value }),
144✔
368
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
112✔
369
});
370

371
/** @todo improve it when OpenAPI 3.1.0 will be released */
372
export const depictTuple: Depicter<z.ZodTuple> = ({
168✔
373
  schema: { items },
374
  next,
375
}) => {
376
  const types = items.map((item) => next({ schema: item }));
72✔
377
  return {
40✔
378
    type: "array",
379
    minItems: types.length,
380
    maxItems: types.length,
381
    items: {
382
      oneOf: types,
383
      format: "tuple",
384
      ...(types.length > 0 && {
72✔
385
        description: types
386
          .map(
387
            (item, index) =>
388
              `${index}: ${isSchemaObject(item) ? item.type : item.$ref}`,
72!
389
          )
390
          .join(", "),
391
      }),
392
    },
393
  };
394
};
395

396
export const depictString: Depicter<z.ZodString> = ({
168✔
397
  schema: {
398
    isEmail,
399
    isURL,
400
    minLength,
401
    maxLength,
402
    isUUID,
403
    isCUID,
404
    isCUID2,
405
    isULID,
406
    isIP,
407
    isEmoji,
408
    isDatetime,
409
    _def: { checks },
410
  },
411
}) => {
412
  const regexCheck = checks.find(
1,432✔
413
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
414
      check.kind === "regex",
320✔
415
  );
416
  const datetimeCheck = checks.find(
1,432✔
417
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
418
      check.kind === "datetime",
328✔
419
  );
420
  const regex = regexCheck
1,432✔
421
    ? regexCheck.regex
422
    : datetimeCheck
1,360✔
423
    ? datetimeCheck.offset
16✔
424
      ? new RegExp(
425
          `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`,
426
        )
427
      : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
428
    : undefined;
429
  return {
1,432✔
430
    type: "string" as const,
431
    ...(isDatetime && { format: "date-time" }),
1,448✔
432
    ...(isEmail && { format: "email" }),
1,456✔
433
    ...(isURL && { format: "url" }),
1,448✔
434
    ...(isUUID && { format: "uuid" }),
1,448✔
435
    ...(isCUID && { format: "cuid" }),
1,448✔
436
    ...(isCUID2 && { format: "cuid2" }),
1,440✔
437
    ...(isULID && { format: "ulid" }),
1,440✔
438
    ...(isIP && { format: "ip" }),
1,440✔
439
    ...(isEmoji && { format: "emoji" }),
1,440✔
440
    ...(minLength !== null && { minLength }),
1,512✔
441
    ...(maxLength !== null && { maxLength }),
1,464✔
442
    ...(regex && { pattern: `/${regex.source}/${regex.flags}` }),
1,520✔
443
  };
444
};
445

446
/** @todo support exclusive min/max as numbers in case of OpenAPI v3.1.x */
447
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
168✔
448
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
344✔
449
    | Extract<z.ZodNumberCheck, { kind: "min" }>
450
    | undefined;
451
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
344✔
452
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
376✔
453
    | Extract<z.ZodNumberCheck, { kind: "max" }>
454
    | undefined;
455
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
344✔
456
  return {
344✔
457
    type: schema.isInt ? ("integer" as const) : ("number" as const),
344✔
458
    format: schema.isInt ? ("int64" as const) : ("double" as const),
344✔
459
    minimum:
460
      schema.minValue === null
344✔
461
        ? schema.isInt
192✔
462
          ? Number.MIN_SAFE_INTEGER
463
          : Number.MIN_VALUE
464
        : schema.minValue,
465
    exclusiveMinimum: !isMinInclusive,
466
    maximum:
467
      schema.maxValue === null
344✔
468
        ? schema.isInt
288✔
469
          ? Number.MAX_SAFE_INTEGER
470
          : Number.MAX_VALUE
471
        : schema.maxValue,
472
    exclusiveMaximum: !isMaxInclusive,
473
  };
474
};
475

476
export const depictObjectProperties = ({
168✔
477
  schema: { shape },
478
  next,
479
}: Parameters<Depicter<z.AnyZodObject>>[0]) => {
480
  return Object.keys(shape).reduce(
2,112✔
481
    (carry, key) => ({
3,368✔
482
      ...carry,
483
      [key]: next({ schema: shape[key] }),
484
    }),
485
    {} as Record<string, SchemaObject | ReferenceObject>,
486
  );
487
};
488

489
const makeSample = (depicted: SchemaObject) => {
168✔
490
  const type = (
491
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
56!
492
  ) as keyof typeof samples;
493
  return samples?.[type];
56✔
494
};
495

496
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
168✔
497
  schema,
498
  isResponse,
499
  next,
500
}) => {
501
  const input = next({ schema: schema.innerType() });
264✔
502
  const { effect } = schema._def;
264✔
503
  if (isResponse && effect.type === "transform" && isSchemaObject(input)) {
264✔
504
    const outputType = tryToTransform({ effect, sample: makeSample(input) });
56✔
505
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
56✔
506
      return { type: outputType as "number" | "string" | "boolean" };
40✔
507
    } else {
508
      return next({ schema: z.any() });
16✔
509
    }
510
  }
511
  if (!isResponse && effect.type === "preprocess" && isSchemaObject(input)) {
208✔
512
    const { type: inputType, ...rest } = input;
24✔
513
    return {
24✔
514
      ...rest,
515
      format: `${rest.format || inputType} (preprocessed)`,
40✔
516
    };
517
  }
518
  return input;
184✔
519
};
520

521
export const depictPipeline: Depicter<z.ZodPipeline<any, any>> = ({
168✔
522
  schema,
523
  isResponse,
524
  next,
525
}) => next({ schema: schema._def[isResponse ? "out" : "in"] });
16✔
526

527
export const depictBranded: Depicter<z.ZodBranded<z.ZodTypeAny, any>> = ({
168✔
528
  schema,
529
  next,
530
}) => next({ schema: schema.unwrap() });
8✔
531

532
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
168✔
533
  next,
534
  schema: lazy,
535
  serializer: serialize,
536
  getRef,
537
  makeRef,
538
}): ReferenceObject => {
539
  const hash = serialize(lazy.schema);
96✔
540
  return (
96✔
541
    getRef(hash) ||
144✔
542
    (() => {
543
      makeRef(hash, {}); // make empty ref first
48✔
544
      return makeRef(hash, next({ schema: lazy.schema })); // update
48✔
545
    })()
546
  );
547
};
548

549
export const depictExamples = (
168✔
550
  schema: z.ZodTypeAny,
551
  isResponse: boolean,
552
  omitProps: string[] = [],
816✔
553
): MediaExamples => {
554
  const examples = getExamples({
1,016✔
555
    schema,
556
    variant: isResponse ? "parsed" : "original",
1,016✔
557
    validate: true,
558
  });
559
  if (examples.length === 0) {
1,016✔
560
    return {};
544✔
561
  }
562
  return {
472✔
563
    examples: examples.reduce<ExamplesObject>(
564
      (carry, example, index) => ({
496✔
565
        ...carry,
566
        [`example${index + 1}`]: <ExampleObject>{
567
          value: omit(omitProps, example),
568
        },
569
      }),
570
      {},
571
    ),
572
  };
573
};
574

575
export const depictParamExamples = (
168✔
576
  schema: z.ZodTypeAny,
577
  isResponse: boolean,
578
  param: string,
579
): MediaExamples => {
580
  const examples = getExamples({
264✔
581
    schema,
582
    variant: isResponse ? "parsed" : "original",
264✔
583
    validate: true,
584
  });
585
  if (examples.length === 0) {
264✔
586
    return {};
224✔
587
  }
588
  return {
40✔
589
    examples: examples.reduce<ExamplesObject>(
590
      (carry, example, index) =>
591
        param in example
56!
592
          ? {
593
              ...carry,
594
              [`example${index + 1}`]: <ExampleObject>{
595
                value: example[param],
596
              },
597
            }
598
          : carry,
599
      {},
600
    ),
601
  };
602
};
603

604
export function extractObjectSchema(
72✔
605
  subject: IOSchema,
606
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">,
607
) {
608
  if (subject instanceof z.ZodObject) {
744✔
609
    return subject;
608✔
610
  }
611
  let objectSchema: z.AnyZodObject;
612
  if (
136✔
613
    subject instanceof z.ZodUnion ||
248✔
614
    subject instanceof z.ZodDiscriminatedUnion
615
  ) {
616
    objectSchema = Array.from(subject.options.values())
32✔
617
      .map((option) => extractObjectSchema(option, ctx))
64✔
618
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
64✔
619
  } else if (subject instanceof z.ZodEffects) {
104✔
620
    if (hasTopLevelTransformingEffect(subject)) {
24✔
621
      throw new DocumentationError({
8✔
622
        message: `Using transformations on the top level of ${
623
          ctx.isResponse ? "response" : "input"
8!
624
        } schema is not allowed.`,
625
        ...ctx,
626
      });
627
    }
628
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
16✔
629
  } else {
630
    // intersection
631
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
80✔
632
      extractObjectSchema(subject._def.right, ctx),
633
    );
634
  }
635
  return copyMeta(subject, objectSchema);
128✔
636
}
637

638
export const depictRequestParams = ({
432✔
639
  path,
640
  method,
641
  endpoint,
642
  inputSources,
643
  serializer,
644
  getRef,
645
  makeRef,
646
  composition,
647
  clue = "parameter",
432✔
648
}: ReqResDepictHelperCommonProps & {
649
  inputSources: InputSource[];
650
}): ParameterObject[] => {
651
  const schema = endpoint.getSchema("input");
432✔
652
  const shape = extractObjectSchema(schema, {
432✔
653
    path,
654
    method,
655
    isResponse: false,
656
  }).shape;
657
  const pathParams = getRoutePathParams(path);
432✔
658
  const isQueryEnabled = inputSources.includes("query");
432✔
659
  const isParamsEnabled = inputSources.includes("params");
432✔
660
  const isPathParam = (name: string) =>
432✔
661
    isParamsEnabled && pathParams.includes(name);
800✔
662
  return Object.keys(shape)
432✔
663
    .filter((name) => isQueryEnabled || isPathParam(name))
768✔
664
    .map((name) => {
665
      const depicted = walkSchema({
248✔
666
        schema: shape[name],
667
        isResponse: false,
668
        rules: depicters,
669
        onEach,
670
        onMissing,
671
        serializer,
672
        getRef,
673
        makeRef,
674
        path,
675
        method,
676
      });
677
      const result =
678
        composition === "components"
248✔
679
          ? makeRef(makeCleanId(path, method, `${clue} ${name}`), depicted)
680
          : depicted;
681
      return {
248✔
682
        name,
683
        in: isPathParam(name) ? "path" : "query",
248✔
684
        required: !shape[name].isOptional(),
685
        description:
686
          (isSchemaObject(depicted) && depicted.description) ||
712✔
687
          `${method.toUpperCase()} ${path} ${clue}`,
688
        schema: result,
689
        ...depictParamExamples(schema, false, name),
690
      };
691
    });
692
};
693

694
export const depicters: HandlingRules<
448✔
695
  SchemaObject | ReferenceObject,
696
  OpenAPIContext
697
> = {
168✔
698
  ZodString: depictString,
699
  ZodNumber: depictNumber,
700
  ZodBigInt: depictBigInt,
701
  ZodBoolean: depictBoolean,
702
  ZodDateIn: depictDateIn,
703
  ZodDateOut: depictDateOut,
704
  ZodNull: depictNull,
705
  ZodArray: depictArray,
706
  ZodTuple: depictTuple,
707
  ZodRecord: depictRecord,
708
  ZodObject: depictObject,
709
  ZodLiteral: depictLiteral,
710
  ZodIntersection: depictIntersection,
711
  ZodUnion: depictUnion,
712
  ZodFile: depictFile,
713
  ZodUpload: depictUpload,
714
  ZodAny: depictAny,
715
  ZodDefault: depictDefault,
716
  ZodEnum: depictEnum,
717
  ZodNativeEnum: depictEnum,
718
  ZodEffects: depictEffect,
719
  ZodOptional: depictOptional,
720
  ZodNullable: depictNullable,
721
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
722
  ZodBranded: depictBranded,
723
  ZodDate: depictDate,
724
  ZodCatch: depictCatch,
725
  ZodPipeline: depictPipeline,
726
  ZodLazy: depictLazy,
727
};
728

729
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
448✔
730
  schema,
731
  isResponse,
732
  prev,
733
}) => {
734
  if (isReferenceObject(prev)) {
5,680✔
735
    return {};
72✔
736
  }
737
  const { description } = schema;
5,608✔
738
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,608✔
739
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,608✔
740
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,608✔
741
  const isActuallyNullable =
742
    !shouldAvoidParsing &&
5,608✔
743
    hasTypePropertyInDepiction &&
744
    !isResponseHavingCoercion &&
745
    schema.isNullable();
746
  const examples = shouldAvoidParsing
5,608!
747
    ? []
748
    : getExamples({
749
        schema,
750
        variant: isResponse ? "parsed" : "original",
5,608✔
751
        validate: true,
752
      });
753
  return {
5,608✔
754
    ...(description && { description }),
5,648✔
755
    ...(isActuallyNullable && { nullable: true }),
5,696✔
756
    ...(examples.length > 0 && { example: examples[0] }),
6,176✔
757
  };
758
};
759

760
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({
448✔
761
  schema,
762
  ...ctx
763
}) => {
764
  throw new DocumentationError({
56✔
765
    message: `Zod type ${schema.constructor.name} is unsupported.`,
766
    ...ctx,
767
  });
768
};
769

770
export const excludeParamsFromDepiction = (
168✔
771
  depicted: SchemaObject | ReferenceObject,
772
  pathParams: string[],
773
): SchemaObject | ReferenceObject => {
774
  if (isReferenceObject(depicted)) {
360✔
775
    return depicted;
8✔
776
  }
777
  const properties = depicted.properties
352✔
778
    ? omit(pathParams, depicted.properties)
779
    : undefined;
780
  const example = depicted.example
352✔
781
    ? omit(pathParams, depicted.example)
782
    : undefined;
783
  const required = depicted.required
352✔
784
    ? depicted.required.filter((name) => !pathParams.includes(name))
520✔
785
    : undefined;
786
  const allOf = depicted.allOf
352✔
787
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
788
        excludeParamsFromDepiction(entry, pathParams),
96✔
789
      )
790
    : undefined;
791
  const oneOf = depicted.oneOf
352✔
792
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
793
        excludeParamsFromDepiction(entry, pathParams),
48✔
794
      )
795
    : undefined;
796

797
  return omit(
352✔
798
    Object.entries({ properties, required, example, allOf, oneOf })
799
      .filter(([{}, value]) => value === undefined)
1,760✔
800
      .map(([key]) => key),
1,080✔
801
    {
802
      ...depicted,
803
      properties,
804
      required,
805
      example,
806
      allOf,
807
      oneOf,
808
    },
809
  );
810
};
811

812
export const excludeExampleFromDepiction = (
168✔
813
  depicted: SchemaObject | ReferenceObject,
814
): SchemaObject | ReferenceObject =>
815
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
1,008!
816

817
export const depictResponse = ({
816✔
818
  method,
819
  path,
820
  endpoint,
821
  isPositive,
822
  serializer,
823
  getRef,
824
  makeRef,
825
  composition,
826
  clue = "response",
×
827
}: ReqResDepictHelperCommonProps & {
828
  isPositive: boolean;
829
}): ResponseObject => {
830
  const schema = endpoint.getSchema(isPositive ? "positive" : "negative");
816✔
831
  const mimeTypes = endpoint.getMimeTypes(isPositive ? "positive" : "negative");
816✔
832
  const depictedSchema = excludeExampleFromDepiction(
816✔
833
    walkSchema({
834
      schema,
835
      isResponse: true,
836
      rules: depicters,
837
      onEach,
838
      onMissing,
839
      serializer,
840
      getRef,
841
      makeRef,
842
      path,
843
      method,
844
    }),
845
  );
846
  const examples = depictExamples(schema, true);
816✔
847
  const result =
848
    composition === "components"
816✔
849
      ? makeRef(makeCleanId(path, method, clue), depictedSchema)
850
      : depictedSchema;
851

852
  return {
816✔
853
    description: `${method.toUpperCase()} ${path} ${clue}`,
854
    content: mimeTypes.reduce(
855
      (carry, mimeType) => ({
824✔
856
        ...carry,
857
        [mimeType]: { schema: result, ...examples },
858
      }),
859
      {} as ContentObject,
860
    ),
861
  };
862
};
863

864
type SecurityHelper<K extends Security["type"]> = (
865
  security: Security & { type: K },
866
) => SecuritySchemeObject;
867

868
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
168✔
869
  type: "http",
870
  scheme: "basic",
871
});
872
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
168✔
873
  format: bearerFormat,
874
}) => ({
32✔
875
  type: "http",
876
  scheme: "bearer",
877
  ...(bearerFormat && { bearerFormat }),
40✔
878
});
879
// @todo add description on actual input placement
880
const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
168✔
881
  type: "apiKey",
882
  in: "query", // body is not supported yet, https://swagger.io/docs/specification/authentication/api-keys/
883
  name,
884
});
885
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
168✔
886
  type: "apiKey",
887
  in: "header",
888
  name,
889
});
890
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
168✔
891
  type: "apiKey",
892
  in: "cookie",
893
  name,
894
});
895
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
168✔
896
  url: openIdConnectUrl,
897
}) => ({
8✔
898
  type: "openIdConnect",
899
  openIdConnectUrl,
900
});
901
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
168✔
902
  type: "oauth2",
903
  flows: (
904
    Object.keys(flows) as (keyof typeof flows)[]
905
  ).reduce<OAuthFlowsObject>((acc, key) => {
906
    const flow = flows[key];
56✔
907
    if (!flow) {
56✔
908
      return acc;
16✔
909
    }
910
    const { scopes = {}, ...rest } = flow;
40!
911
    return { ...acc, [key]: { ...rest, scopes } };
40✔
912
  }, {}),
913
});
914

915
export const depictSecurity = (
392✔
916
  container: LogicalContainer<Security>,
917
): LogicalContainer<SecuritySchemeObject> => {
918
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
392✔
919
    basic: depictBasicSecurity,
920
    bearer: depictBearerSecurity,
921
    input: depictInputSecurity,
922
    header: depictHeaderSecurity,
923
    cookie: depictCookieSecurity,
924
    openid: depictOpenIdSecurity,
925
    oauth2: depictOAuth2Security,
926
  };
927
  return mapLogicalContainer(container, (security) =>
392✔
928
    (methods[security.type] as SecurityHelper<typeof security.type>)(security),
144✔
929
  );
930
};
931

932
export const depictSecurityRefs = (
400✔
933
  container: LogicalContainer<{ name: string; scopes: string[] }>,
934
): SecurityRequirementObject[] => {
935
  if (typeof container === "object") {
768✔
936
    if ("or" in container) {
768✔
937
      return container.or.map((entry) =>
400✔
938
        ("and" in entry
144✔
939
          ? entry.and
940
          : [entry]
941
        ).reduce<SecurityRequirementObject>(
942
          (agg, { name, scopes }) => ({
208✔
943
            ...agg,
944
            [name]: scopes,
945
          }),
946
          {},
947
        ),
948
      );
949
    }
950
    if ("and" in container) {
368✔
951
      return depictSecurityRefs(andToOr(container));
360✔
952
    }
953
  }
954
  return depictSecurityRefs({ or: [container] });
8✔
955
};
956

957
export const depictRequest = ({
240✔
958
  method,
959
  path,
960
  endpoint,
961
  serializer,
962
  getRef,
963
  makeRef,
964
  composition,
965
  clue = "request body",
240✔
966
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
967
  const pathParams = getRoutePathParams(path);
240✔
968
  const bodyDepiction = excludeExampleFromDepiction(
240✔
969
    excludeParamsFromDepiction(
970
      walkSchema({
971
        schema: endpoint.getSchema("input"),
972
        isResponse: false,
973
        rules: depicters,
974
        onEach,
975
        onMissing,
976
        serializer,
977
        getRef,
978
        makeRef,
979
        path,
980
        method,
981
      }),
982
      pathParams,
983
    ),
984
  );
985
  const bodyExamples = depictExamples(
184✔
986
    endpoint.getSchema("input"),
987
    false,
988
    pathParams,
989
  );
990
  const result =
991
    composition === "components"
184✔
992
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
993
      : bodyDepiction;
994

995
  return {
184✔
996
    description: `${method.toUpperCase()} ${path} ${clue}`,
997
    content: endpoint.getMimeTypes("input").reduce(
998
      (carry, mimeType) => ({
184✔
999
        ...carry,
1000
        [mimeType]: { schema: result, ...bodyExamples },
1001
      }),
1002
      {} as ContentObject,
1003
    ),
1004
  };
1005
};
1006

1007
export const depictTags = <TAG extends string>(
168✔
1008
  tags: TagsConfig<TAG>,
1009
): TagObject[] =>
1010
  (Object.keys(tags) as TAG[]).map((tag) => {
32✔
1011
    const def = tags[tag];
64✔
1012
    return {
64✔
1013
      name: tag,
1014
      description: typeof def === "string" ? def : def.description,
64✔
1015
      ...(typeof def === "object" &&
88✔
1016
        def.url && { externalDocs: { url: def.url } }),
1017
    };
1018
  });
1019

1020
export const ensureShortDescription = (description: string) => {
168✔
1021
  if (description.length <= shortDescriptionLimit) {
128✔
1022
    return description;
104✔
1023
  }
1024
  return description.slice(0, shortDescriptionLimit - 1) + "…";
24✔
1025
};
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