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

RobinTail / express-zod-api / 7434048251

06 Jan 2024 09:28PM CUT coverage: 100.0%. Remained the same
7434048251

Pull #1454

github

web-flow
Merge b3e42226e into cb74c3f05
Pull Request #1454: Bump @typescript-eslint/parser from 6.17.0 to 6.18.0

679 of 710 branches covered (0.0%)

1169 of 1169 relevant lines covered (100.0%)

460.85 hits per line

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

100.0
/src/documentation.ts
1
import assert from "node:assert/strict";
2
import {
3
  OpenApiBuilder,
4
  OperationObject,
5
  ReferenceObject,
6
  SchemaObject,
7
  SecuritySchemeObject,
8
  SecuritySchemeType,
9
} from "openapi3-ts/oas31";
10
import { z } from "zod";
11
import { DocumentationError } from "./errors";
12
import {
13
  defaultInputSources,
14
  defaultSerializer,
15
  makeCleanId,
16
} from "./common-helpers";
17
import { CommonConfig } from "./config-type";
18
import { mapLogicalContainer } from "./logical-container";
19
import { Method } from "./method";
20
import {
21
  depictRequest,
22
  depictRequestParams,
23
  depictResponse,
24
  depictSecurity,
25
  depictSecurityRefs,
26
  depictTags,
27
  ensureShortDescription,
28
  reformatParamsInPath,
29
} from "./documentation-helpers";
30
import { Routing } from "./routing";
31
import { RoutingWalkerParams, walkRouting } from "./routing-walker";
32

33
type Component =
34
  | "positiveResponse"
35
  | "negativeResponse"
36
  | "requestParameter"
37
  | "requestBody";
38

39
/** @desc user defined function that creates a component description from its properties */
40
type Descriptor = (
41
  props: Record<"method" | "path" | "operationId", string> & {
42
    statusCode?: number; // for response only
43
  },
44
) => string;
45

46
interface DocumentationParams {
47
  title: string;
48
  version: string;
49
  serverUrl: string | [string, ...string[]];
50
  routing: Routing;
51
  config: CommonConfig;
52
  /**
53
   * @desc Descriptions of various components based on their properties (method, path, operationId).
54
   * @desc When composition set to "components", component name is generated from this description
55
   * @default () => `${method} ${path} ${component}`
56
   * */
57
  descriptions?: Partial<Record<Component, Descriptor>>;
58
  /** @default true */
59
  hasSummaryFromDescription?: boolean;
60
  /** @default inline */
61
  composition?: "inline" | "components";
62
  /**
63
   * @desc Used for comparing schemas wrapped into z.lazy() to limit the recursion
64
   * @default JSON.stringify() + SHA1 hash as a hex digest
65
   * */
66
  serializer?: (schema: z.ZodTypeAny) => string;
67
}
68

69
export class Documentation extends OpenApiBuilder {
70
  protected lastSecuritySchemaIds: Partial<Record<SecuritySchemeType, number>> =
71
    {};
276✔
72
  protected lastOperationIdSuffixes: Record<string, number> = {};
276✔
73

74
  protected makeRef(
75
    name: string,
76
    schema: SchemaObject | ReferenceObject,
77
  ): ReferenceObject {
78
    this.addSchema(name, schema);
240✔
79
    return this.getRef(name)!;
240✔
80
  }
81

82
  protected getRef(name: string): ReferenceObject | undefined {
83
    return name in (this.rootDoc.components?.schemas || {})
276!
84
      ? { $ref: `#/components/schemas/${name}` }
85
      : undefined;
86
  }
87

88
  protected ensureUniqOperationId(
89
    path: string,
90
    method: Method,
91
    userDefinedOperationId?: string,
92
  ) {
93
    if (userDefinedOperationId) {
396✔
94
      assert(
30✔
95
        !(userDefinedOperationId in this.lastOperationIdSuffixes),
96
        new DocumentationError({
97
          message: `Duplicated operationId: "${userDefinedOperationId}"`,
98
          method,
99
          isResponse: false,
100
          path,
101
        }),
102
      );
103
      this.lastOperationIdSuffixes[userDefinedOperationId] = 1;
24✔
104
      return userDefinedOperationId;
24✔
105
    }
106
    const operationId = makeCleanId(method, path);
366✔
107
    if (operationId in this.lastOperationIdSuffixes) {
366✔
108
      this.lastOperationIdSuffixes[operationId]++;
6✔
109
      return `${operationId}${this.lastOperationIdSuffixes[operationId]}`;
6✔
110
    }
111
    this.lastOperationIdSuffixes[operationId] = 1;
360✔
112
    return operationId;
360✔
113
  }
114

115
  protected ensureUniqSecuritySchemaName(subject: SecuritySchemeObject) {
116
    const serializedSubject = JSON.stringify(subject);
54✔
117
    for (const name in this.rootDoc.components?.securitySchemes || {}) {
54!
118
      if (
60✔
119
        serializedSubject ===
120
        JSON.stringify(this.rootDoc.components?.securitySchemes?.[name])
121
      ) {
122
        return name;
6✔
123
      }
124
    }
125
    this.lastSecuritySchemaIds[subject.type] =
48✔
126
      (this.lastSecuritySchemaIds?.[subject.type] || 0) + 1;
78✔
127
    return `${subject.type.toUpperCase()}_${
48✔
128
      this.lastSecuritySchemaIds[subject.type]
129
    }`;
130
  }
131

132
  public constructor({
133
    routing,
134
    config,
135
    title,
136
    version,
137
    serverUrl,
138
    descriptions,
139
    hasSummaryFromDescription = true,
276✔
140
    composition = "inline",
246✔
141
    serializer = defaultSerializer,
276✔
142
  }: DocumentationParams) {
143
    super();
276✔
144
    this.addInfo({ title, version });
276✔
145
    for (const url of typeof serverUrl === "string" ? [serverUrl] : serverUrl) {
276!
146
      this.addServer({ url });
276✔
147
    }
148
    const onEndpoint: RoutingWalkerParams["onEndpoint"] = (
276✔
149
      endpoint,
150
      path,
151
      _method,
152
    ) => {
153
      const method = _method as Method;
396✔
154
      const commonParams = {
396✔
155
        path,
156
        method,
157
        endpoint,
158
        composition,
159
        serializer,
160
        getRef: this.getRef.bind(this),
161
        makeRef: this.makeRef.bind(this),
162
      };
163
      const [shortDesc, longDesc] = (["short", "long"] as const).map(
396✔
164
        endpoint.getDescription.bind(endpoint),
165
      );
166
      const inputSources =
167
        config.inputSources?.[method] || defaultInputSources[method];
396✔
168
      const operationId = this.ensureUniqOperationId(
396✔
169
        path,
170
        method,
171
        endpoint.getOperationId(method),
172
      );
173
      const depictedParams = depictRequestParams({
390✔
174
        ...commonParams,
175
        inputSources,
176
        schema: endpoint.getSchema("input"),
177
        description: descriptions?.requestParameter?.call(null, {
178
          method,
179
          path,
180
          operationId,
181
        }),
182
      });
183

184
      const operation: OperationObject = { operationId, responses: {} };
390✔
185
      for (const variant of ["positive", "negative"] as const) {
390✔
186
        const responses = endpoint.getResponses(variant);
780✔
187
        for (const { mimeTypes, schema, statusCodes } of responses) {
780✔
188
          for (const statusCode of statusCodes) {
804✔
189
            operation.responses[statusCode] = depictResponse({
828✔
190
              ...commonParams,
191
              variant,
192
              schema,
193
              mimeTypes,
194
              statusCode,
195
              hasMultipleStatusCodes:
196
                responses.length > 1 || statusCodes.length > 1,
1,596✔
197
              description: descriptions?.[`${variant}Response`]?.call(null, {
198
                method,
199
                path,
200
                operationId,
201
                statusCode,
202
              }),
203
            });
204
          }
205
        }
206
      }
207

208
      if (longDesc) {
390✔
209
        operation.description = longDesc;
72✔
210
        if (hasSummaryFromDescription && shortDesc === undefined) {
72✔
211
          operation.summary = ensureShortDescription(longDesc);
60✔
212
        }
213
      }
214
      if (shortDesc) {
390✔
215
        operation.summary = ensureShortDescription(shortDesc);
36✔
216
      }
217
      if (endpoint.getTags().length > 0) {
390✔
218
        operation.tags = endpoint.getTags();
96✔
219
      }
220
      if (depictedParams.length > 0) {
390✔
221
        operation.parameters = depictedParams;
138✔
222
      }
223
      if (inputSources.includes("body")) {
390✔
224
        operation.requestBody = depictRequest({
228✔
225
          ...commonParams,
226
          schema: endpoint.getSchema("input"),
227
          mimeTypes: endpoint.getMimeTypes("input"),
228
          description: descriptions?.requestBody?.call(null, {
229
            method,
230
            path,
231
            operationId,
232
          }),
233
        });
234
      }
235
      const securityRefs = depictSecurityRefs(
348✔
236
        mapLogicalContainer(
237
          depictSecurity(endpoint.getSecurity(), inputSources),
238
          (securitySchema) => {
239
            const name = this.ensureUniqSecuritySchemaName(securitySchema);
54✔
240
            const scopes = ["oauth2", "openIdConnect"].includes(
54✔
241
              securitySchema.type,
242
            )
243
              ? endpoint.getScopes()
244
              : [];
245
            this.addSecurityScheme(name, securitySchema);
54✔
246
            return { name, scopes };
54✔
247
          },
248
        ),
249
      );
250
      if (securityRefs.length > 0) {
348✔
251
        operation.security = securityRefs;
30✔
252
      }
253
      this.addPath(reformatParamsInPath(path), { [method]: operation });
348✔
254
    };
255
    walkRouting({ routing, onEndpoint });
276✔
256
    this.rootDoc.tags = config.tags ? depictTags(config.tags) : [];
228✔
257
  }
258
}
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