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

RobinTail / express-zod-api / 3863662923

pending completion
3863662923

Pull #767

github

GitHub
Merge c715ccae3 into 31d217f5b
Pull Request #767: Bump @typescript-eslint/eslint-plugin from 5.47.1 to 5.48.0

437 of 464 branches covered (94.18%)

1039 of 1039 relevant lines covered (100.0%)

345.46 hits per line

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

96.84
/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
  hasTopLevelTransformingEffect,
22
  routePathParamsRegex,
23
} from "./common-helpers";
24
import { InputSources, TagsConfig } from "./config-type";
25
import { ZodDateIn, isoDateRegex } from "./date-in-schema";
176✔
26
import { ZodDateOut } from "./date-out-schema";
27
import { AbstractEndpoint } from "./endpoint";
28
import { OpenAPIError } from "./errors";
176✔
29
import { ZodFile } from "./file-schema";
30
import { IOSchema } from "./io-schema";
31
import {
176✔
32
  LogicalContainer,
33
  andToOr,
34
  mapLogicalContainer,
35
} from "./logical-container";
36
import { copyMeta } from "./metadata";
176✔
37
import { Method } from "./method";
38
import {
176✔
39
  HandlingRules,
40
  HandlingVariant,
41
  SchemaHandler,
42
  walkSchema,
43
} from "./schema-walker";
44
import { Security } from "./security";
45
import { ZodUpload } from "./upload-schema";
46

47
type MediaExamples = Pick<MediaTypeObject, "examples">;
48

49
export interface OpenAPIContext {
50
  isResponse: boolean;
51
}
52

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

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

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

68
/* eslint-disable @typescript-eslint/no-use-before-define */
69

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

73
export const depictDefault: Depicter<z.ZodDefault<z.ZodTypeAny>> = ({
176✔
74
  schema: {
75
    _def: { innerType, defaultValue },
76
  },
77
  next,
78
}) => ({
24✔
79
  ...next({ schema: innerType }),
80
  default: defaultValue(),
81
});
82

83
export const depictCatch: Depicter<z.ZodCatch<z.ZodTypeAny>> = ({
176✔
84
  schema: {
85
    _def: { innerType },
86
  },
87
  next,
88
}) => next({ schema: innerType });
8✔
89

90
export const depictAny: Depicter<z.ZodAny> = () => ({
176✔
91
  format: "any",
92
});
93

94
export const depictUpload: Depicter<ZodUpload> = ({ isResponse }) => {
176✔
95
  if (isResponse) {
24✔
96
    throw new OpenAPIError("Please use z.upload() only for input.");
8✔
97
  }
98
  return {
16✔
99
    type: "string",
100
    format: "binary",
101
  };
102
};
103

104
export const depictFile: Depicter<ZodFile> = ({
176✔
105
  schema: { isBinary, isBase64 },
106
  isResponse,
107
}) => {
108
  if (!isResponse) {
40✔
109
    throw new OpenAPIError("Please use z.file() only within ResultHandler.");
8✔
110
  }
111
  return {
32✔
112
    type: "string",
113
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
48✔
114
  };
115
};
116

117
export const depictUnion: Depicter<
176✔
118
  z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
119
> = ({ schema: { options }, next }) => ({
176✔
120
  oneOf: options.map((option) => next({ schema: option })),
144✔
121
});
122

123
export const depictDiscriminatedUnion: Depicter<
176✔
124
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
125
> = ({ schema: { options, discriminator }, next }) => {
176✔
126
  return {
24✔
127
    discriminator: { propertyName: discriminator },
128
    oneOf: Array.from(options.values()).map((option) =>
129
      next({ schema: option })
48✔
130
    ),
131
  };
132
};
133

134
export const depictIntersection: Depicter<
176✔
135
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
136
> = ({
176✔
137
  schema: {
138
    _def: { left, right },
139
  },
140
  next,
141
}) => ({
56✔
142
  allOf: [left, right].map((entry) => next({ schema: entry })),
112✔
143
});
144

145
export const depictOptional: Depicter<z.ZodOptional<any>> = ({
176✔
146
  schema,
147
  next,
148
}) => next({ schema: schema.unwrap() });
80✔
149

150
export const depictNullable: Depicter<z.ZodNullable<any>> = ({
176✔
151
  schema,
152
  next,
153
}) => ({
48✔
154
  nullable: true,
155
  ...next({ schema: schema.unwrap() }),
156
});
157

158
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
176✔
159
  schema,
160
}) => ({
32✔
161
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
162
  enum: Object.values(schema.enum),
163
});
164

165
export const depictLiteral: Depicter<z.ZodLiteral<any>> = ({
176✔
166
  schema: { value },
167
}) => ({
712✔
168
  type: typeof value as "string" | "number" | "boolean",
169
  enum: [value],
170
});
171

172
export const depictObject: Depicter<z.AnyZodObject> = ({
176✔
173
  schema,
174
  isResponse,
175
  next,
176
}) => ({
1,640✔
177
  type: "object",
178
  properties: depictObjectProperties({ schema, isResponse, next }),
179
  required: Object.keys(schema.shape).filter((key) => {
180
    const prop = schema.shape[key];
2,520✔
181
    const isOptional =
182
      isResponse && hasCoercion(prop)
2,520✔
183
        ? prop instanceof z.ZodOptional
2,520✔
184
        : prop.isOptional();
185
    return !isOptional;
2,520✔
186
  }),
187
});
188

189
/**
190
 * @see https://swagger.io/docs/specification/data-models/data-types/
191
 * @todo use type:"null" for OpenAPI 3.1
192
 * */
193
export const depictNull: Depicter<z.ZodNull> = () => ({
176✔
194
  type: "string",
195
  nullable: true,
196
  format: "null",
197
});
198

199
export const depictDateIn: Depicter<ZodDateIn> = ({ isResponse }) => {
176✔
200
  if (isResponse) {
32✔
201
    throw new OpenAPIError("Please use z.dateOut() for output.");
8✔
202
  }
203
  return {
24✔
204
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
205
    type: "string",
206
    format: "date-time",
207
    pattern: isoDateRegex.source,
208
    externalDocs: {
209
      url: isoDateDocumentationUrl,
210
    },
211
  };
212
};
213

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

228
/** @throws OpenAPIError */
229
export const depictDate: Depicter<z.ZodDate> = ({ isResponse }) => {
176✔
230
  throw new OpenAPIError(
16✔
231
    `Using z.date() within ${
232
      isResponse ? "output" : "input"
16✔
233
    } schema is forbidden. Please use z.date${
234
      isResponse ? "Out" : "In"
16✔
235
    }() instead. Check out the documentation for details.`
236
  );
237
};
238

239
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
240✔
240
  type: "boolean",
241
});
242

243
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
176✔
244
  type: "integer",
245
  format: "bigint",
246
});
247

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

318
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
176✔
319
  schema: { _def: def, element },
320
  next,
321
}) => ({
56✔
322
  type: "array",
323
  items: next({ schema: element }),
324
  ...(def.minLength !== null && { minItems: def.minLength.value }),
96✔
325
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
64✔
326
});
327

328
/** @todo improve it when OpenAPI 3.1.0 will be released */
329
export const depictTuple: Depicter<z.ZodTuple> = ({
176✔
330
  schema: { items },
331
  next,
332
}) => {
333
  const types = items.map((item) => next({ schema: item }));
72✔
334
  return {
40✔
335
    type: "array",
336
    minItems: types.length,
337
    maxItems: types.length,
338
    items: {
339
      oneOf: types,
340
      format: "tuple",
341
      ...(types.length > 0 && {
72✔
342
        description: types
343
          .map((item, index) => `${index}: ${item.type}`)
72✔
344
          .join(", "),
345
      }),
346
    },
347
  };
348
};
349

350
export const depictString: Depicter<z.ZodString> = ({
176✔
351
  schema: {
352
    isEmail,
353
    isURL,
354
    minLength,
355
    maxLength,
356
    isUUID,
357
    isCUID,
358
    isDatetime,
359
    _def: { checks },
360
  },
361
}) => {
362
  const regexCheck = checks.find(
1,120✔
363
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
364
      check.kind === "regex"
240✔
365
  );
366
  const datetimeCheck = checks.find(
1,120✔
367
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
368
      check.kind === "datetime"
248✔
369
  );
370
  const regex = regexCheck
1,120✔
371
    ? regexCheck.regex
1,120✔
372
    : datetimeCheck
373
    ? datetimeCheck.offset
1,072✔
374
      ? new RegExp(
16✔
375
          `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`
376
        )
377
      : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
378
    : undefined;
379
  return {
1,120✔
380
    type: "string" as const,
381
    ...(isDatetime && { format: "date-time" }),
1,136✔
382
    ...(isEmail && { format: "email" }),
1,144✔
383
    ...(isURL && { format: "url" }),
1,136✔
384
    ...(isUUID && { format: "uuid" }),
1,136✔
385
    ...(isCUID && { format: "cuid" }),
1,136✔
386
    ...(minLength !== null && { minLength }),
1,184✔
387
    ...(maxLength !== null && { maxLength }),
1,152✔
388
    ...(regex && { pattern: `/${regex.source}/${regex.flags}` }),
1,184✔
389
  };
390
};
391

392
/** @todo support exclusive min/max as numbers in case of OpenAPI v3.1.x */
393
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
176✔
394
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
312✔
395
    | Extract<ArrayElement<z.ZodNumberDef["checks"]>, { kind: "min" }>
396
    | undefined;
397
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
304✔
398
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
344✔
399
    | Extract<ArrayElement<z.ZodNumberDef["checks"]>, { kind: "max" }>
400
    | undefined;
401
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
304✔
402
  return {
304✔
403
    type: schema.isInt ? ("integer" as const) : ("number" as const),
304✔
404
    format: schema.isInt ? ("int64" as const) : ("double" as const),
304✔
405
    minimum:
406
      schema.minValue === null
407
        ? schema.isInt
304✔
408
          ? Number.MIN_SAFE_INTEGER
168✔
409
          : Number.MIN_VALUE
410
        : schema.minValue,
411
    exclusiveMinimum: !isMinInclusive,
412
    maximum:
413
      schema.maxValue === null
414
        ? schema.isInt
304✔
415
          ? Number.MAX_SAFE_INTEGER
248✔
416
          : Number.MAX_VALUE
417
        : schema.maxValue,
418
    exclusiveMaximum: !isMaxInclusive,
419
  };
420
};
421

422
export const depictObjectProperties = ({
176✔
423
  schema: { shape },
424
  next,
425
}: Parameters<Depicter<z.AnyZodObject>>[0]) => {
426
  return Object.keys(shape).reduce(
1,696✔
427
    (carry, key) => ({
2,680✔
428
      ...carry,
429
      [key]: next({ schema: shape[key] }),
430
    }),
431
    {} as Record<string, SchemaObject>
432
  );
433
};
434

435
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
176✔
436
  schema,
437
  isResponse,
438
  next,
439
}) => {
440
  const input = next({ schema: schema._def.schema });
184✔
441
  const effect = schema._def.effect;
184✔
442
  if (isResponse && effect && effect.type === "transform") {
184✔
443
    let output = "undefined";
40✔
444
    try {
40✔
445
      output = typeof effect.transform(
40✔
446
        ["integer", "number"].includes(`${input.type}`)
447
          ? 0
40✔
448
          : "string" === input.type
449
          ? ""
16!
450
          : "boolean" === input.type
451
          ? false
×
452
          : "object" === input.type
453
          ? {}
×
454
          : "null" === input.type
455
          ? null
×
456
          : "array" === input.type
457
          ? []
×
458
          : undefined,
459
        { addIssue: () => {}, path: [] }
460
      );
461
    } catch (e) {
462
      /**/
463
    }
464
    return {
40✔
465
      ...input,
466
      ...(["number", "string", "boolean"].includes(output) && {
80✔
467
        type: output as "number" | "string" | "boolean",
468
      }),
469
    };
470
  }
471
  if (!isResponse && effect && effect.type === "preprocess") {
144✔
472
    const { type: inputType, ...rest } = input;
24✔
473
    return {
24✔
474
      ...rest,
475
      format: `${rest.format || inputType} (preprocessed)`,
40✔
476
    };
477
  }
478
  return input;
120✔
479
};
480

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

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

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

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

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

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

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

633
/**
634
 * @desc isNullable() and isOptional() validate the schema's input
635
 * @desc They always return true in case of coercion, which should be taken into account when depicting response
636
 */
637
export const hasCoercion = (schema: z.ZodType): boolean =>
176✔
638
  "coerce" in schema._def && typeof schema._def.coerce === "boolean"
2,064✔
639
    ? schema._def.coerce
2,064✔
640
    : false;
641

642
export const onEach: Depicter<z.ZodTypeAny, "last"> = ({
176✔
643
  schema,
644
  isResponse,
645
}) => {
646
  const { description } = schema;
4,592✔
647
  const examples = getExamples(schema, isResponse);
4,592✔
648
  return {
4,592✔
649
    ...(description && { description }),
4,616✔
650
    ...(schema.isNullable() &&
4,904✔
651
      !(isResponse && hasCoercion(schema)) && { nullable: true }),
272✔
652
    ...(examples.length > 0 && { example: examples[0] }),
5,024✔
653
  };
654
};
655

656
export const onMissing = (schema: z.ZodTypeAny) => {
176✔
657
  throw new OpenAPIError(`Zod type ${schema.constructor.name} is unsupported`);
64✔
658
};
659

660
export const excludeParamsFromDepiction = (
176✔
661
  depicted: SchemaObject,
662
  pathParams: string[]
663
): SchemaObject => {
664
  const properties = depicted.properties
280✔
665
    ? omit(pathParams, depicted.properties)
280✔
666
    : undefined;
667
  const example = depicted.example
280✔
668
    ? omit(pathParams, depicted.example)
280✔
669
    : undefined;
670
  const required = depicted.required
280✔
671
    ? depicted.required.filter((name) => !pathParams.includes(name))
432✔
672
    : undefined;
673
  const allOf = depicted.allOf
280✔
674
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
280✔
675
        excludeParamsFromDepiction(entry, pathParams)
64✔
676
      )
677
    : undefined;
678
  const oneOf = depicted.oneOf
280✔
679
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
280✔
680
        excludeParamsFromDepiction(entry, pathParams)
48✔
681
      )
682
    : undefined;
683

684
  return omit(
280✔
685
    Object.entries({ properties, required, example, allOf, oneOf })
686
      .filter(([{}, value]) => value === undefined)
1,400✔
687
      .map(([key]) => key),
840✔
688
    {
689
      ...depicted,
690
      properties,
691
      required,
692
      example,
693
      allOf,
694
      oneOf,
695
    }
696
  );
697
};
698

699
export const excludeExampleFromDepiction = (
176✔
700
  depicted: SchemaObject
701
): SchemaObject => omit(["example"], depicted);
776✔
702

703
export const depictResponse = ({
176✔
704
  method,
705
  path,
706
  description,
707
  endpoint,
708
  isPositive,
709
}: ReqResDepictHelperCommonProps & {
710
  description: string;
711
  isPositive: boolean;
712
}): ResponseObject => {
713
  const schema = isPositive
624✔
714
    ? endpoint.getPositiveResponseSchema()
624✔
715
    : endpoint.getNegativeResponseSchema();
716
  const mimeTypes = isPositive
624✔
717
    ? endpoint.getPositiveMimeTypes()
624✔
718
    : endpoint.getNegativeMimeTypes();
719
  const depictedSchema = excludeExampleFromDepiction(
624✔
720
    walkSchema({
721
      schema,
722
      isResponse: true,
723
      rules: depicters,
724
      onEach,
725
      onMissing,
726
    })
727
  );
728
  const examples = depictIOExamples(schema, true);
624✔
729

730
  return {
624✔
731
    description: `${method.toUpperCase()} ${path} ${description}`,
732
    content: mimeTypes.reduce(
733
      (carry, mimeType) => ({
624✔
734
        ...carry,
735
        [mimeType]: {
736
          schema: depictedSchema,
737
          ...examples,
738
        },
739
      }),
740
      {} as ContentObject
741
    ),
742
  };
743
};
744

745
type SecurityHelper<K extends Security["type"]> = (
746
  security: Security & { type: K }
747
) => SecuritySchemeObject;
748

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

796
export const depictSecurity = (
176✔
797
  container: LogicalContainer<Security>
798
): LogicalContainer<SecuritySchemeObject> => {
799
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
288✔
800
    basic: depictBasicSecurity,
801
    bearer: depictBearerSecurity,
802
    input: depictInputSecurity,
803
    header: depictHeaderSecurity,
804
    cookie: depictCookieSecurity,
805
    openid: depictOpenIdSecurity,
806
    oauth2: depictOAuth2Security,
807
  };
808
  return mapLogicalContainer(container, (security) =>
288✔
809
    (methods[security.type] as SecurityHelper<typeof security.type>)(security)
120✔
810
  );
811
};
812

813
export const depictSecurityRefs = (
176✔
814
  container: LogicalContainer<{ name: string; scopes: string[] }>
815
): SecurityRequirementObject[] => {
816
  if (typeof container === "object") {
560✔
817
    if ("or" in container) {
560✔
818
      return container.or.map((entry) =>
296✔
819
        ("and" in entry
128✔
820
          ? entry.and
128✔
821
          : [entry]
822
        ).reduce<SecurityRequirementObject>(
823
          (agg, { name, scopes }) => ({
184✔
824
            ...agg,
825
            [name]: scopes,
826
          }),
827
          {}
828
        )
829
      );
830
    }
831
    if ("and" in container) {
264✔
832
      return depictSecurityRefs(andToOr(container));
256✔
833
    }
834
  }
835
  return depictSecurityRefs({ or: [container] });
8✔
836
};
837

838
export const depictRequest = ({
176✔
839
  method,
840
  path,
841
  endpoint,
842
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
843
  const pathParams = getRoutePathParams(path);
208✔
844
  const bodyDepiction = excludeExampleFromDepiction(
208✔
845
    excludeParamsFromDepiction(
846
      walkSchema({
847
        schema: endpoint.getInputSchema(),
848
        isResponse: false,
849
        rules: depicters,
850
        onEach,
851
        onMissing,
852
      }),
853
      pathParams
854
    )
855
  );
856
  const bodyExamples = depictIOExamples(
144✔
857
    endpoint.getInputSchema(),
858
    false,
859
    pathParams
860
  );
861

862
  return {
144✔
863
    content: endpoint.getInputMimeTypes().reduce(
864
      (carry, mimeType) => ({
144✔
865
        ...carry,
866
        [mimeType]: {
867
          schema: {
868
            description: `${method.toUpperCase()} ${path} request body`,
869
            ...bodyDepiction,
870
          },
871
          ...bodyExamples,
872
        },
873
      }),
874
      {} as ContentObject
875
    ),
876
  };
877
};
878

879
export const depictTags = <TAG extends string>(
176✔
880
  tags: TagsConfig<TAG>
881
): TagObject[] =>
882
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
883
    const def = tags[tag];
48✔
884
    return {
48✔
885
      name: tag,
886
      description: typeof def === "string" ? def : def.description,
48✔
887
      ...(typeof def === "object" &&
72✔
888
        def.url && { externalDocs: { url: def.url } }),
889
    };
890
  });
891

892
export const ensureShortDescription = (description: string) => {
176✔
893
  if (description.length <= shortDescriptionLimit) {
72✔
894
    return description;
56✔
895
  }
896
  return description.slice(0, shortDescriptionLimit - 1) + "…";
16✔
897
};
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