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

RobinTail / express-zod-api / 4029019205

pending completion
4029019205

Pull #792

github

GitHub
Merge 3057b90a9 into 977273f58
Pull Request #792: Bump @typescript-eslint/parser from 5.48.2 to 5.49.0

442 of 462 branches covered (95.67%)

1057 of 1057 relevant lines covered (100.0%)

341.74 hits per line

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

98.91
/src/open-api-helpers.ts
1
import {
2
  ContentObject,
3
  ExampleObject,
4
  ExamplesObject,
5
  MediaTypeObject,
6
  OAuthFlowsObject,
7
  ParameterObject,
8
  RequestBodyObject,
9
  ResponseObject,
10
  SchemaObject,
11
  SecurityRequirementObject,
12
  SecuritySchemeObject,
13
  TagObject,
14
} from "openapi3-ts";
15
import { omit } from "ramda";
176✔
16
import { z } from "zod";
176✔
17
import {
176✔
18
  ArrayElement,
19
  getExamples,
20
  getRoutePathParams,
21
  hasCoercion,
22
  hasTopLevelTransformingEffect,
23
  routePathParamsRegex,
24
  tryToTransform,
25
} from "./common-helpers";
26
import { InputSources, TagsConfig } from "./config-type";
27
import { ZodDateIn, isoDateRegex } from "./date-in-schema";
176✔
28
import { ZodDateOut } from "./date-out-schema";
29
import { AbstractEndpoint } from "./endpoint";
30
import { OpenAPIError } from "./errors";
176✔
31
import { ZodFile } from "./file-schema";
32
import { IOSchema } from "./io-schema";
33
import {
176✔
34
  LogicalContainer,
35
  andToOr,
36
  mapLogicalContainer,
37
} from "./logical-container";
38
import { copyMeta } from "./metadata";
176✔
39
import { Method } from "./method";
40
import {
176✔
41
  HandlingRules,
42
  HandlingVariant,
43
  SchemaHandler,
44
  walkSchema,
45
} from "./schema-walker";
46
import { Security } from "./security";
47
import { ZodUpload } from "./upload-schema";
48

49
type MediaExamples = Pick<MediaTypeObject, "examples">;
50

51
export interface OpenAPIContext {
52
  isResponse: boolean;
53
}
54

55
type Depicter<
56
  T extends z.ZodTypeAny,
57
  Variant extends HandlingVariant = "regular"
58
> = SchemaHandler<T, SchemaObject, OpenAPIContext, Variant>;
59

60
interface ReqResDepictHelperCommonProps {
61
  method: Method;
62
  path: string;
63
  endpoint: AbstractEndpoint;
64
}
65

66
const shortDescriptionLimit = 50;
176✔
67
const isoDateDocumentationUrl =
68
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
176✔
69

70
const samples: Record<
71
  Exclude<NonNullable<SchemaObject["type"]>, Array<any>>,
72
  any
73
> = {
176✔
74
  integer: 0,
75
  number: 0,
76
  string: "",
77
  boolean: false,
78
  object: {},
79
  null: null,
80
  array: [],
81
};
82

83
/* eslint-disable @typescript-eslint/no-use-before-define */
84

85
export const reformatParamsInPath = (path: string) =>
176✔
86
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
280✔
87

88
export const depictDefault: Depicter<z.ZodDefault<z.ZodTypeAny>> = ({
176✔
89
  schema: {
90
    _def: { innerType, defaultValue },
91
  },
92
  next,
93
}) => ({
24✔
94
  ...next({ schema: innerType }),
95
  default: defaultValue(),
96
});
97

98
export const depictCatch: Depicter<z.ZodCatch<z.ZodTypeAny>> = ({
176✔
99
  schema: {
100
    _def: { innerType },
101
  },
102
  next,
103
}) => next({ schema: innerType });
8✔
104

105
export const depictAny: Depicter<z.ZodAny> = () => ({
176✔
106
  format: "any",
107
});
108

109
export const depictUpload: Depicter<ZodUpload> = ({ isResponse }) => {
176✔
110
  if (isResponse) {
24✔
111
    throw new OpenAPIError("Please use z.upload() only for input.");
8✔
112
  }
113
  return {
16✔
114
    type: "string",
115
    format: "binary",
116
  };
117
};
118

119
export const depictFile: Depicter<ZodFile> = ({
176✔
120
  schema: { isBinary, isBase64 },
121
  isResponse,
122
}) => {
123
  if (!isResponse) {
40✔
124
    throw new OpenAPIError("Please use z.file() only within ResultHandler.");
8✔
125
  }
126
  return {
32✔
127
    type: "string",
128
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
48✔
129
  };
130
};
131

132
export const depictUnion: Depicter<
176✔
133
  z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
134
> = ({ schema: { options }, next }) => ({
176✔
135
  oneOf: options.map((option) => next({ schema: option })),
144✔
136
});
137

138
export const depictDiscriminatedUnion: Depicter<
176✔
139
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
140
> = ({ schema: { options, discriminator }, next }) => {
176✔
141
  return {
24✔
142
    discriminator: { propertyName: discriminator },
143
    oneOf: Array.from(options.values()).map((option) =>
144
      next({ schema: option })
48✔
145
    ),
146
  };
147
};
148

149
export const depictIntersection: Depicter<
176✔
150
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
151
> = ({
176✔
152
  schema: {
153
    _def: { left, right },
154
  },
155
  next,
156
}) => ({
56✔
157
  allOf: [left, right].map((entry) => next({ schema: entry })),
112✔
158
});
159

160
export const depictOptional: Depicter<z.ZodOptional<any>> = ({
176✔
161
  schema,
162
  next,
163
}) => next({ schema: schema.unwrap() });
80✔
164

165
export const depictNullable: Depicter<z.ZodNullable<any>> = ({
176✔
166
  schema,
167
  next,
168
}) => ({
48✔
169
  nullable: true,
170
  ...next({ schema: schema.unwrap() }),
171
});
172

173
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
176✔
174
  schema,
175
}) => ({
32✔
176
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
177
  enum: Object.values(schema.enum),
178
});
179

180
export const depictLiteral: Depicter<z.ZodLiteral<any>> = ({
176✔
181
  schema: { value },
182
}) => ({
712✔
183
  type: typeof value as "string" | "number" | "boolean",
184
  enum: [value],
185
});
186

187
export const depictObject: Depicter<z.AnyZodObject> = ({
176✔
188
  schema,
189
  isResponse,
190
  next,
191
}) => ({
1,640✔
192
  type: "object",
193
  properties: depictObjectProperties({ schema, isResponse, next }),
194
  required: Object.keys(schema.shape).filter((key) => {
195
    const prop = schema.shape[key];
2,520✔
196
    const isOptional =
197
      isResponse && hasCoercion(prop)
2,520✔
198
        ? prop instanceof z.ZodOptional
199
        : prop.isOptional();
200
    return !isOptional;
2,520✔
201
  }),
202
});
203

204
/**
205
 * @see https://swagger.io/docs/specification/data-models/data-types/
206
 * @todo use type:"null" for OpenAPI 3.1
207
 * */
208
export const depictNull: Depicter<z.ZodNull> = () => ({
176✔
209
  type: "string",
210
  nullable: true,
211
  format: "null",
212
});
213

214
export const depictDateIn: Depicter<ZodDateIn> = ({ isResponse }) => {
176✔
215
  if (isResponse) {
32✔
216
    throw new OpenAPIError("Please use z.dateOut() for output.");
8✔
217
  }
218
  return {
24✔
219
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
220
    type: "string",
221
    format: "date-time",
222
    pattern: isoDateRegex.source,
223
    externalDocs: {
224
      url: isoDateDocumentationUrl,
225
    },
226
  };
227
};
228

229
export const depictDateOut: Depicter<ZodDateOut> = ({ isResponse }) => {
176✔
230
  if (!isResponse) {
32✔
231
    throw new OpenAPIError("Please use z.dateIn() for input.");
8✔
232
  }
233
  return {
24✔
234
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
235
    type: "string",
236
    format: "date-time",
237
    externalDocs: {
238
      url: isoDateDocumentationUrl,
239
    },
240
  };
241
};
242

243
/** @throws OpenAPIError */
244
export const depictDate: Depicter<z.ZodDate> = ({ isResponse }) => {
176✔
245
  throw new OpenAPIError(
16✔
246
    `Using z.date() within ${
247
      isResponse ? "output" : "input"
16✔
248
    } schema is forbidden. Please use z.date${
249
      isResponse ? "Out" : "In"
16✔
250
    }() instead. Check out the documentation for details.`
251
  );
252
};
253

254
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
240✔
255
  type: "boolean",
256
});
257

258
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
176✔
259
  type: "integer",
260
  format: "bigint",
261
});
262

263
export const depictRecord: Depicter<z.ZodRecord<z.ZodTypeAny>> = ({
176✔
264
  schema: { keySchema, valueSchema },
265
  isResponse,
266
  next,
267
}) => {
268
  if (keySchema instanceof z.ZodEnum || keySchema instanceof z.ZodNativeEnum) {
96✔
269
    const keys = Object.values(keySchema.enum) as string[];
16✔
270
    const shape = keys.reduce(
16✔
271
      (carry, key) => ({
32✔
272
        ...carry,
273
        [key]: valueSchema,
274
      }),
275
      {} as z.ZodRawShape
276
    );
277
    return {
16✔
278
      type: "object",
279
      properties: depictObjectProperties({
280
        schema: z.object(shape),
281
        isResponse,
282
        next,
283
      }),
284
      required: keys,
285
    };
286
  }
287
  if (keySchema instanceof z.ZodLiteral) {
80✔
288
    return {
16✔
289
      type: "object",
290
      properties: depictObjectProperties({
291
        schema: z.object({
292
          [keySchema.value]: valueSchema,
293
        }),
294
        isResponse,
295
        next,
296
      }),
297
      required: [keySchema.value],
298
    };
299
  }
300
  if (keySchema instanceof z.ZodUnion) {
64✔
301
    const areOptionsLiteral = keySchema.options.reduce(
16✔
302
      (carry: boolean, option: z.ZodTypeAny) =>
303
        carry && option instanceof z.ZodLiteral,
32✔
304
      true
305
    );
306
    if (areOptionsLiteral) {
16✔
307
      const shape = keySchema.options.reduce(
16✔
308
        (carry: z.ZodRawShape, option: z.ZodLiteral<any>) => ({
32✔
309
          ...carry,
310
          [option.value]: valueSchema,
311
        }),
312
        {} as z.ZodRawShape
313
      );
314
      return {
16✔
315
        type: "object",
316
        properties: depictObjectProperties({
317
          schema: z.object(shape),
318
          isResponse,
319
          next,
320
        }),
321
        required: keySchema.options.map(
322
          (option: z.ZodLiteral<any>) => option.value
32✔
323
        ),
324
      };
325
    }
326
  }
327
  return {
48✔
328
    type: "object",
329
    additionalProperties: next({ schema: valueSchema }),
330
  };
331
};
332

333
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
176✔
334
  schema: { _def: def, element },
335
  next,
336
}) => ({
56✔
337
  type: "array",
338
  items: next({ schema: element }),
339
  ...(def.minLength !== null && { minItems: def.minLength.value }),
96✔
340
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
64✔
341
});
342

343
/** @todo improve it when OpenAPI 3.1.0 will be released */
344
export const depictTuple: Depicter<z.ZodTuple> = ({
176✔
345
  schema: { items },
346
  next,
347
}) => {
348
  const types = items.map((item) => next({ schema: item }));
72✔
349
  return {
40✔
350
    type: "array",
351
    minItems: types.length,
352
    maxItems: types.length,
353
    items: {
354
      oneOf: types,
355
      format: "tuple",
356
      ...(types.length > 0 && {
72✔
357
        description: types
358
          .map((item, index) => `${index}: ${item.type}`)
72✔
359
          .join(", "),
360
      }),
361
    },
362
  };
363
};
364

365
export const depictString: Depicter<z.ZodString> = ({
176✔
366
  schema: {
367
    isEmail,
368
    isURL,
369
    minLength,
370
    maxLength,
371
    isUUID,
372
    isCUID,
373
    isDatetime,
374
    _def: { checks },
375
  },
376
}) => {
377
  const regexCheck = checks.find(
1,120✔
378
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
379
      check.kind === "regex"
240✔
380
  );
381
  const datetimeCheck = checks.find(
1,120✔
382
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
383
      check.kind === "datetime"
248✔
384
  );
385
  const regex = regexCheck
1,120✔
386
    ? regexCheck.regex
387
    : datetimeCheck
1,072✔
388
    ? datetimeCheck.offset
16✔
389
      ? new RegExp(
390
          `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`
391
        )
392
      : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
393
    : undefined;
394
  return {
1,120✔
395
    type: "string" as const,
396
    ...(isDatetime && { format: "date-time" }),
1,136✔
397
    ...(isEmail && { format: "email" }),
1,144✔
398
    ...(isURL && { format: "url" }),
1,136✔
399
    ...(isUUID && { format: "uuid" }),
1,136✔
400
    ...(isCUID && { format: "cuid" }),
1,136✔
401
    ...(minLength !== null && { minLength }),
1,184✔
402
    ...(maxLength !== null && { maxLength }),
1,152✔
403
    ...(regex && { pattern: `/${regex.source}/${regex.flags}` }),
1,184✔
404
  };
405
};
406

407
/** @todo support exclusive min/max as numbers in case of OpenAPI v3.1.x */
408
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
176✔
409
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
320✔
410
    | Extract<ArrayElement<z.ZodNumberDef["checks"]>, { kind: "min" }>
411
    | undefined;
412
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
320✔
413
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
344✔
414
    | Extract<ArrayElement<z.ZodNumberDef["checks"]>, { kind: "max" }>
415
    | undefined;
416
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
320✔
417
  return {
320✔
418
    type: schema.isInt ? ("integer" as const) : ("number" as const),
320✔
419
    format: schema.isInt ? ("int64" as const) : ("double" as const),
320✔
420
    minimum:
421
      schema.minValue === null
320✔
422
        ? schema.isInt
184✔
423
          ? Number.MIN_SAFE_INTEGER
424
          : Number.MIN_VALUE
425
        : schema.minValue,
426
    exclusiveMinimum: !isMinInclusive,
427
    maximum:
428
      schema.maxValue === null
320✔
429
        ? schema.isInt
264✔
430
          ? Number.MAX_SAFE_INTEGER
431
          : Number.MAX_VALUE
432
        : schema.maxValue,
433
    exclusiveMaximum: !isMaxInclusive,
434
  };
435
};
436

437
export const depictObjectProperties = ({
176✔
438
  schema: { shape },
439
  next,
440
}: Parameters<Depicter<z.AnyZodObject>>[0]) => {
441
  return Object.keys(shape).reduce(
1,696✔
442
    (carry, key) => ({
2,680✔
443
      ...carry,
444
      [key]: next({ schema: shape[key] }),
445
    }),
446
    {} as Record<string, SchemaObject>
447
  );
448
};
449

450
const makeSample = (depicted: SchemaObject) => {
176✔
451
  const type = (
452
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
56!
453
  ) as keyof typeof samples;
454
  return samples?.[type];
56✔
455
};
456

457
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
176✔
458
  schema,
459
  isResponse,
460
  next,
461
}) => {
462
  const input = next({ schema: schema.innerType() });
200✔
463
  const { effect } = schema._def;
200✔
464
  if (isResponse && effect.type === "transform") {
200✔
465
    const outputType = tryToTransform({ effect, sample: makeSample(input) });
56✔
466
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
56✔
467
      return { type: outputType as "number" | "string" | "boolean" };
40✔
468
    } else {
469
      return next({ schema: z.any() });
16✔
470
    }
471
  }
472
  if (!isResponse && effect.type === "preprocess") {
144✔
473
    const { type: inputType, ...rest } = input;
24✔
474
    return {
24✔
475
      ...rest,
476
      format: `${rest.format || inputType} (preprocessed)`,
40✔
477
    };
478
  }
479
  return input;
120✔
480
};
481

482
export const depictPipeline: Depicter<z.ZodPipeline<any, any>> = ({
176✔
483
  schema,
484
  isResponse,
485
  next,
486
}) => next({ schema: schema._def[isResponse ? "out" : "in"] });
16✔
487

488
export const depictBranded: Depicter<z.ZodBranded<z.ZodTypeAny, any>> = ({
176✔
489
  schema,
490
  next,
491
}) => next({ schema: schema.unwrap() });
8✔
492

493
export const depictIOExamples = <T extends IOSchema>(
176✔
494
  schema: T,
495
  isResponse: boolean,
496
  omitProps: string[] = []
624✔
497
): MediaExamples => {
498
  const examples = getExamples(schema, isResponse);
784✔
499
  if (examples.length === 0) {
784✔
500
    return {};
416✔
501
  }
502
  return {
368✔
503
    examples: examples.reduce<ExamplesObject>(
504
      (carry, example, index) => ({
384✔
505
        ...carry,
506
        [`example${index + 1}`]: <ExampleObject>{
507
          value: omit(omitProps, example),
508
        },
509
      }),
510
      {}
511
    ),
512
  };
513
};
514

515
export const depictIOParamExamples = <T extends IOSchema>(
176✔
516
  schema: T,
517
  isResponse: boolean,
518
  param: string
519
): MediaExamples => {
520
  const examples = getExamples(schema, isResponse);
224✔
521
  if (examples.length === 0) {
224✔
522
    return {};
192✔
523
  }
524
  return {
32✔
525
    examples: examples.reduce<ExamplesObject>(
526
      (carry, example, index) =>
527
        param in example
48!
528
          ? {
529
              ...carry,
530
              [`example${index + 1}`]: <ExampleObject>{
531
                value: example[param],
532
              },
533
            }
534
          : carry,
535
      {}
536
    ),
537
  };
538
};
539

540
export function extractObjectSchema(subject: IOSchema) {
176✔
541
  if (subject instanceof z.ZodObject) {
600✔
542
    return subject;
488✔
543
  }
544
  let objectSchema: z.AnyZodObject;
545
  if (
112✔
546
    subject instanceof z.ZodUnion ||
200✔
547
    subject instanceof z.ZodDiscriminatedUnion
548
  ) {
549
    objectSchema = Array.from(subject.options.values())
32✔
550
      .map((option) => extractObjectSchema(option))
64✔
551
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
64✔
552
  } else if (subject instanceof z.ZodEffects) {
80✔
553
    if (hasTopLevelTransformingEffect(subject)) {
24✔
554
      throw new OpenAPIError(
8✔
555
        "Using transformations on the top level of input schema is not allowed."
556
      );
557
    }
558
    objectSchema = extractObjectSchema(subject._def.schema); // object refinement
16✔
559
  } else {
560
    // intersection
561
    objectSchema = extractObjectSchema(subject._def.left).merge(
56✔
562
      extractObjectSchema(subject._def.right)
563
    );
564
  }
565
  return copyMeta(subject, objectSchema);
104✔
566
}
567

568
export const depictRequestParams = ({
176✔
569
  path,
570
  method,
571
  endpoint,
572
  inputSources,
573
}: ReqResDepictHelperCommonProps & {
574
  inputSources: InputSources[Method];
575
}): ParameterObject[] => {
576
  const schema = endpoint.getInputSchema();
336✔
577
  const shape = extractObjectSchema(schema).shape;
336✔
578
  const pathParams = getRoutePathParams(path);
336✔
579
  const isQueryEnabled = inputSources.includes("query");
336✔
580
  const isParamsEnabled = inputSources.includes("params");
336✔
581
  const isPathParam = (name: string) =>
336✔
582
    isParamsEnabled && pathParams.includes(name);
680✔
583
  return Object.keys(shape)
336✔
584
    .filter((name) => isQueryEnabled || isPathParam(name))
656✔
585
    .map((name) => ({
208✔
586
      name,
587
      in: isPathParam(name) ? "path" : "query",
208✔
588
      required: !shape[name].isOptional(),
589
      schema: {
590
        description: `${method.toUpperCase()} ${path} parameter`,
591
        ...walkSchema({
592
          schema: shape[name],
593
          isResponse: false,
594
          rules: depicters,
595
          onEach,
596
          onMissing,
597
        }),
598
      },
599
      ...depictIOParamExamples(schema, false, name),
600
    }));
601
};
602

603
export const depicters: HandlingRules<SchemaObject, OpenAPIContext> = {
176✔
604
  ZodString: depictString,
605
  ZodNumber: depictNumber,
606
  ZodBigInt: depictBigInt,
607
  ZodBoolean: depictBoolean,
608
  ZodDateIn: depictDateIn,
609
  ZodDateOut: depictDateOut,
610
  ZodNull: depictNull,
611
  ZodArray: depictArray,
612
  ZodTuple: depictTuple,
613
  ZodRecord: depictRecord,
614
  ZodObject: depictObject,
615
  ZodLiteral: depictLiteral,
616
  ZodIntersection: depictIntersection,
617
  ZodUnion: depictUnion,
618
  ZodFile: depictFile,
619
  ZodUpload: depictUpload,
620
  ZodAny: depictAny,
621
  ZodDefault: depictDefault,
622
  ZodEnum: depictEnum,
623
  ZodNativeEnum: depictEnum,
624
  ZodEffects: depictEffect,
625
  ZodOptional: depictOptional,
626
  ZodNullable: depictNullable,
627
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
628
  ZodBranded: depictBranded,
629
  ZodDate: depictDate,
630
  ZodCatch: depictCatch,
631
  ZodPipeline: depictPipeline,
632
};
633

634
export const onEach: Depicter<z.ZodTypeAny, "last"> = ({
176✔
635
  schema,
636
  isResponse,
637
}) => {
638
  const { description } = schema;
4,624✔
639
  const examples = getExamples(schema, isResponse);
4,624✔
640
  return {
4,624✔
641
    ...(description && { description }),
4,648✔
642
    ...(schema.isNullable() &&
4,968✔
643
      !(isResponse && hasCoercion(schema)) && { nullable: true }),
304✔
644
    ...(examples.length > 0 && { example: examples[0] }),
5,056✔
645
  };
646
};
647

648
export const onMissing = (schema: z.ZodTypeAny) => {
176✔
649
  throw new OpenAPIError(`Zod type ${schema.constructor.name} is unsupported`);
64✔
650
};
651

652
export const excludeParamsFromDepiction = (
176✔
653
  depicted: SchemaObject,
654
  pathParams: string[]
655
): SchemaObject => {
656
  const properties = depicted.properties
280✔
657
    ? omit(pathParams, depicted.properties)
658
    : undefined;
659
  const example = depicted.example
280✔
660
    ? omit(pathParams, depicted.example)
661
    : undefined;
662
  const required = depicted.required
280✔
663
    ? depicted.required.filter((name) => !pathParams.includes(name))
432✔
664
    : undefined;
665
  const allOf = depicted.allOf
280✔
666
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
667
        excludeParamsFromDepiction(entry, pathParams)
64✔
668
      )
669
    : undefined;
670
  const oneOf = depicted.oneOf
280✔
671
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
672
        excludeParamsFromDepiction(entry, pathParams)
48✔
673
      )
674
    : undefined;
675

676
  return omit(
280✔
677
    Object.entries({ properties, required, example, allOf, oneOf })
678
      .filter(([{}, value]) => value === undefined)
1,400✔
679
      .map(([key]) => key),
840✔
680
    {
681
      ...depicted,
682
      properties,
683
      required,
684
      example,
685
      allOf,
686
      oneOf,
687
    }
688
  );
689
};
690

691
export const excludeExampleFromDepiction = (
176✔
692
  depicted: SchemaObject
693
): SchemaObject => omit(["example"], depicted);
776✔
694

695
export const depictResponse = ({
176✔
696
  method,
697
  path,
698
  description,
699
  endpoint,
700
  isPositive,
701
}: ReqResDepictHelperCommonProps & {
702
  description: string;
703
  isPositive: boolean;
704
}): ResponseObject => {
705
  const schema = isPositive
624✔
706
    ? endpoint.getPositiveResponseSchema()
707
    : endpoint.getNegativeResponseSchema();
708
  const mimeTypes = isPositive
624✔
709
    ? endpoint.getPositiveMimeTypes()
710
    : endpoint.getNegativeMimeTypes();
711
  const depictedSchema = excludeExampleFromDepiction(
624✔
712
    walkSchema({
713
      schema,
714
      isResponse: true,
715
      rules: depicters,
716
      onEach,
717
      onMissing,
718
    })
719
  );
720
  const examples = depictIOExamples(schema, true);
624✔
721

722
  return {
624✔
723
    description: `${method.toUpperCase()} ${path} ${description}`,
724
    content: mimeTypes.reduce(
725
      (carry, mimeType) => ({
624✔
726
        ...carry,
727
        [mimeType]: {
728
          schema: depictedSchema,
729
          ...examples,
730
        },
731
      }),
732
      {} as ContentObject
733
    ),
734
  };
735
};
736

737
type SecurityHelper<K extends Security["type"]> = (
738
  security: Security & { type: K }
739
) => SecuritySchemeObject;
740

741
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
176✔
742
  type: "http",
743
  scheme: "basic",
744
});
745
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
176✔
746
  format: bearerFormat,
747
}) => ({
24✔
748
  type: "http",
749
  scheme: "bearer",
750
  ...(bearerFormat && { bearerFormat }),
24!
751
});
752
// @todo add description on actual input placement
753
const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
176✔
754
  type: "apiKey",
755
  in: "query", // body is not supported yet, https://swagger.io/docs/specification/authentication/api-keys/
756
  name,
757
});
758
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
176✔
759
  type: "apiKey",
760
  in: "header",
761
  name,
762
});
763
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
176✔
764
  type: "apiKey",
765
  in: "cookie",
766
  name,
767
});
768
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
176✔
769
  url: openIdConnectUrl,
770
}) => ({
8✔
771
  type: "openIdConnect",
772
  openIdConnectUrl,
773
});
774
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
176✔
775
  type: "oauth2",
776
  flows: (
777
    Object.keys(flows) as (keyof typeof flows)[]
778
  ).reduce<OAuthFlowsObject>((acc, key) => {
779
    const flow = flows[key];
56✔
780
    if (!flow) {
56✔
781
      return acc;
16✔
782
    }
783
    const { scopes = {}, ...rest } = flow;
40!
784
    return { ...acc, [key]: { ...rest, scopes } };
40✔
785
  }, {}),
786
});
787

788
export const depictSecurity = (
176✔
789
  container: LogicalContainer<Security>
790
): LogicalContainer<SecuritySchemeObject> => {
791
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
288✔
792
    basic: depictBasicSecurity,
793
    bearer: depictBearerSecurity,
794
    input: depictInputSecurity,
795
    header: depictHeaderSecurity,
796
    cookie: depictCookieSecurity,
797
    openid: depictOpenIdSecurity,
798
    oauth2: depictOAuth2Security,
799
  };
800
  return mapLogicalContainer(container, (security) =>
288✔
801
    (methods[security.type] as SecurityHelper<typeof security.type>)(security)
120✔
802
  );
803
};
804

805
export const depictSecurityRefs = (
176✔
806
  container: LogicalContainer<{ name: string; scopes: string[] }>
807
): SecurityRequirementObject[] => {
808
  if (typeof container === "object") {
560✔
809
    if ("or" in container) {
560✔
810
      return container.or.map((entry) =>
296✔
811
        ("and" in entry
128✔
812
          ? entry.and
813
          : [entry]
814
        ).reduce<SecurityRequirementObject>(
815
          (agg, { name, scopes }) => ({
184✔
816
            ...agg,
817
            [name]: scopes,
818
          }),
819
          {}
820
        )
821
      );
822
    }
823
    if ("and" in container) {
264✔
824
      return depictSecurityRefs(andToOr(container));
256✔
825
    }
826
  }
827
  return depictSecurityRefs({ or: [container] });
8✔
828
};
829

830
export const depictRequest = ({
176✔
831
  method,
832
  path,
833
  endpoint,
834
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
835
  const pathParams = getRoutePathParams(path);
208✔
836
  const bodyDepiction = excludeExampleFromDepiction(
208✔
837
    excludeParamsFromDepiction(
838
      walkSchema({
839
        schema: endpoint.getInputSchema(),
840
        isResponse: false,
841
        rules: depicters,
842
        onEach,
843
        onMissing,
844
      }),
845
      pathParams
846
    )
847
  );
848
  const bodyExamples = depictIOExamples(
144✔
849
    endpoint.getInputSchema(),
850
    false,
851
    pathParams
852
  );
853

854
  return {
144✔
855
    content: endpoint.getInputMimeTypes().reduce(
856
      (carry, mimeType) => ({
144✔
857
        ...carry,
858
        [mimeType]: {
859
          schema: {
860
            description: `${method.toUpperCase()} ${path} request body`,
861
            ...bodyDepiction,
862
          },
863
          ...bodyExamples,
864
        },
865
      }),
866
      {} as ContentObject
867
    ),
868
  };
869
};
870

871
export const depictTags = <TAG extends string>(
176✔
872
  tags: TagsConfig<TAG>
873
): TagObject[] =>
874
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
875
    const def = tags[tag];
48✔
876
    return {
48✔
877
      name: tag,
878
      description: typeof def === "string" ? def : def.description,
48✔
879
      ...(typeof def === "object" &&
72✔
880
        def.url && { externalDocs: { url: def.url } }),
881
    };
882
  });
883

884
export const ensureShortDescription = (description: string) => {
176✔
885
  if (description.length <= shortDescriptionLimit) {
72✔
886
    return description;
56✔
887
  }
888
  return description.slice(0, shortDescriptionLimit - 1) + "…";
16✔
889
};
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