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

RobinTail / express-zod-api / 5980016138

25 Aug 2023 08:09PM CUT coverage: 100.0%. Remained the same
5980016138

push

github

web-flow
Upgrading checkout action in OpenAPI validation workflow

582 of 609 branches covered (0.0%)

1169 of 1169 relevant lines covered (100.0%)

491.85 hits per line

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

100.0
/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 depictReadonly: Depicter<z.ZodReadonly<any>> = ({
168✔
188
  schema,
189
  next,
190
}) => next({ schema: schema._def.innerType });
16✔
191

192
export const depictNullable: Depicter<z.ZodNullable<any>> = ({
168✔
193
  schema,
194
  next,
195
}) => ({
48✔
196
  nullable: true,
197
  ...next({ schema: schema.unwrap() }),
198
});
199

200
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
168✔
201
  schema,
202
}) => ({
32✔
203
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
204
  enum: Object.values(schema.enum),
205
});
206

207
export const depictLiteral: Depicter<z.ZodLiteral<any>> = ({
168✔
208
  schema: { value },
209
}) => ({
872✔
210
  type: typeof value as "string" | "number" | "boolean",
211
  enum: [value],
212
});
213

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

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

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

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

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

291
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
240✔
292
  type: "boolean",
293
});
294

295
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
168✔
296
  type: "integer",
297
  format: "bigint",
298
});
299

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

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

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

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

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

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

494
const makeSample = (depicted: SchemaObject) => {
168✔
495
  const type = (
496
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
56!
497
  ) as keyof typeof samples;
498
  return samples?.[type];
56✔
499
};
500

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

526
export const depictPipeline: Depicter<z.ZodPipeline<any, any>> = ({
168✔
527
  schema,
528
  isResponse,
529
  next,
530
}) => next({ schema: schema._def[isResponse ? "out" : "in"] });
16✔
531

532
export const depictBranded: Depicter<z.ZodBranded<z.ZodTypeAny, any>> = ({
168✔
533
  schema,
534
  next,
535
}) => next({ schema: schema.unwrap() });
8✔
536

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

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

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

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

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

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

735
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
456✔
736
  schema,
737
  isResponse,
738
  prev,
739
}) => {
740
  if (isReferenceObject(prev)) {
5,696✔
741
    return {};
72✔
742
  }
743
  const { description } = schema;
5,624✔
744
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,624✔
745
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,624✔
746
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,624✔
747
  const isActuallyNullable =
748
    !shouldAvoidParsing &&
5,624✔
749
    hasTypePropertyInDepiction &&
750
    !isResponseHavingCoercion &&
751
    schema.isNullable();
752
  const examples = shouldAvoidParsing
5,624!
753
    ? []
754
    : getExamples({
755
        schema,
756
        variant: isResponse ? "parsed" : "original",
5,624✔
757
        validate: true,
758
      });
759
  return {
5,624✔
760
    ...(description && { description }),
5,664✔
761
    ...(isActuallyNullable && { nullable: true }),
5,712✔
762
    ...(examples.length > 0 && { example: examples[0] }),
6,192✔
763
  };
764
};
765

766
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({
456✔
767
  schema,
768
  ...ctx
769
}) => {
770
  throw new DocumentationError({
56✔
771
    message: `Zod type ${schema.constructor.name} is unsupported.`,
772
    ...ctx,
773
  });
774
};
775

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

803
  return omit(
352✔
804
    Object.entries({ properties, required, example, allOf, oneOf })
805
      .filter(([{}, value]) => value === undefined)
1,760✔
806
      .map(([key]) => key),
1,080✔
807
    {
808
      ...depicted,
809
      properties,
810
      required,
811
      example,
812
      allOf,
813
      oneOf,
814
    },
815
  );
816
};
817

818
export const excludeExampleFromDepiction = (
168✔
819
  depicted: SchemaObject | ReferenceObject,
820
): SchemaObject | ReferenceObject =>
821
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
1,008!
822

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

858
  return {
816✔
859
    description: `${method.toUpperCase()} ${path} ${clue}`,
860
    content: mimeTypes.reduce(
861
      (carry, mimeType) => ({
824✔
862
        ...carry,
863
        [mimeType]: { schema: result, ...examples },
864
      }),
865
      {} as ContentObject,
866
    ),
867
  };
868
};
869

870
type SecurityHelper<K extends Security["type"]> = (
871
  security: Security & { type: K },
872
) => SecuritySchemeObject;
873

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

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

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

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

1001
  return {
184✔
1002
    description: `${method.toUpperCase()} ${path} ${clue}`,
1003
    content: endpoint.getMimeTypes("input").reduce(
1004
      (carry, mimeType) => ({
184✔
1005
        ...carry,
1006
        [mimeType]: { schema: result, ...bodyExamples },
1007
      }),
1008
      {} as ContentObject,
1009
    ),
1010
  };
1011
};
1012

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

1026
export const ensureShortDescription = (description: string) => {
168✔
1027
  if (description.length <= shortDescriptionLimit) {
128✔
1028
    return description;
104✔
1029
  }
1030
  return description.slice(0, shortDescriptionLimit - 1) + "…";
24✔
1031
};
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