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

RobinTail / express-zod-api / 4168807755

pending completion
4168807755

Pull #817

github

GitHub
Merge 751b49a76 into 89948c308
Pull Request #817: Bump @typescript-eslint/parser from 5.51.0 to 5.52.0

450 of 470 branches covered (95.74%)

1064 of 1064 relevant lines covered (100.0%)

348.33 hits per line

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

98.93
/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)}}`);
288✔
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
}) => ({
64✔
157
  allOf: [left, right].map((entry) => next({ schema: entry })),
128✔
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
}) => ({
728✔
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
}) => {
192
  const required = Object.keys(schema.shape).filter((key) => {
1,688✔
193
    const prop = schema.shape[key];
2,624✔
194
    const isOptional =
195
      isResponse && hasCoercion(prop)
2,624✔
196
        ? prop instanceof z.ZodOptional
197
        : prop.isOptional();
198
    return !isOptional;
2,624✔
199
  });
200
  return {
1,688✔
201
    type: "object",
202
    properties: depictObjectProperties({ schema, isResponse, next }),
203
    ...(required.length ? { required } : {}),
1,624✔
204
  };
205
};
206

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

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

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

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

257
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
240✔
258
  type: "boolean",
259
});
260

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

679
  return omit(
304✔
680
    Object.entries({ properties, required, example, allOf, oneOf })
681
      .filter(([{}, value]) => value === undefined)
1,520✔
682
      .map(([key]) => key),
960✔
683
    {
684
      ...depicted,
685
      properties,
686
      required,
687
      example,
688
      allOf,
689
      oneOf,
690
    }
691
  );
692
};
693

694
export const excludeExampleFromDepiction = (
176✔
695
  depicted: SchemaObject
696
): SchemaObject => omit(["example"], depicted);
800✔
697

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

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

740
type SecurityHelper<K extends Security["type"]> = (
741
  security: Security & { type: K }
742
) => SecuritySchemeObject;
743

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

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

808
export const depictSecurityRefs = (
176✔
809
  container: LogicalContainer<{ name: string; scopes: string[] }>
810
): SecurityRequirementObject[] => {
811
  if (typeof container === "object") {
576✔
812
    if ("or" in container) {
576✔
813
      return container.or.map((entry) =>
304✔
814
        ("and" in entry
136✔
815
          ? entry.and
816
          : [entry]
817
        ).reduce<SecurityRequirementObject>(
818
          (agg, { name, scopes }) => ({
192✔
819
            ...agg,
820
            [name]: scopes,
821
          }),
822
          {}
823
        )
824
      );
825
    }
826
    if ("and" in container) {
272✔
827
      return depictSecurityRefs(andToOr(container));
264✔
828
    }
829
  }
830
  return depictSecurityRefs({ or: [container] });
8✔
831
};
832

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

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

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

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