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

RobinTail / express-zod-api / 7264049075

19 Dec 2023 03:35PM CUT coverage: 100.0%. Remained the same
7264049075

Pull #1399

github

web-flow
Merge 465ec4337 into ac3789c5a
Pull Request #1399: Bump @typescript-eslint/eslint-plugin from 6.14.0 to 6.15.0

675 of 703 branches covered (0.0%)

1079 of 1079 relevant lines covered (100.0%)

433.08 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/oas30";
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
interface DocumentationParams {
34
  title: string;
35
  version: string;
36
  serverUrl: string | [string, ...string[]];
37
  routing: Routing;
38
  config: CommonConfig;
39
  /** @default Successful response */
40
  successfulResponseDescription?: string;
41
  /** @default Error response */
42
  errorResponseDescription?: string;
43
  /** @default true */
44
  hasSummaryFromDescription?: boolean;
45
  /** @default inline */
46
  composition?: "inline" | "components";
47
  /**
48
   * @desc Used for comparing schemas wrapped into z.lazy() to limit the recursion
49
   * @default JSON.stringify() + SHA1 hash as a hex digest
50
   * */
51
  serializer?: (schema: z.ZodTypeAny) => string;
52
}
53

54
export class Documentation extends OpenApiBuilder {
55
  protected lastSecuritySchemaIds: Partial<Record<SecuritySchemeType, number>> =
56
    {};
258✔
57
  protected lastOperationIdSuffixes: Record<string, number> = {};
258✔
58

59
  protected makeRef(
60
    name: string,
61
    schema: SchemaObject | ReferenceObject,
62
  ): ReferenceObject {
63
    this.addSchema(name, schema);
180✔
64
    return this.getRef(name)!;
180✔
65
  }
66

67
  protected getRef(name: string): ReferenceObject | undefined {
68
    return name in (this.rootDoc.components?.schemas || {})
216!
69
      ? { $ref: `#/components/schemas/${name}` }
70
      : undefined;
71
  }
72

73
  protected ensureUniqOperationId(
74
    path: string,
75
    method: Method,
76
    userDefinedOperationId?: string,
77
  ) {
78
    if (userDefinedOperationId) {
366✔
79
      assert(
30✔
80
        !(userDefinedOperationId in this.lastOperationIdSuffixes),
81
        new DocumentationError({
82
          message: `Duplicated operationId: "${userDefinedOperationId}"`,
83
          method,
84
          isResponse: false,
85
          path,
86
        }),
87
      );
88
      this.lastOperationIdSuffixes[userDefinedOperationId] = 1;
24✔
89
      return userDefinedOperationId;
24✔
90
    }
91
    const operationId = makeCleanId(path, method);
336✔
92
    if (operationId in this.lastOperationIdSuffixes) {
336✔
93
      this.lastOperationIdSuffixes[operationId]++;
6✔
94
      return `${operationId}${this.lastOperationIdSuffixes[operationId]}`;
6✔
95
    }
96
    this.lastOperationIdSuffixes[operationId] = 1;
330✔
97
    return operationId;
330✔
98
  }
99

100
  protected ensureUniqSecuritySchemaName(subject: SecuritySchemeObject) {
101
    const serializedSubject = JSON.stringify(subject);
54✔
102
    for (const name in this.rootDoc.components?.securitySchemes || {}) {
54!
103
      if (
60✔
104
        serializedSubject ===
105
        JSON.stringify(this.rootDoc.components?.securitySchemes?.[name])
106
      ) {
107
        return name;
6✔
108
      }
109
    }
110
    this.lastSecuritySchemaIds[subject.type] =
48✔
111
      (this.lastSecuritySchemaIds?.[subject.type] || 0) + 1;
78✔
112
    return `${subject.type.toUpperCase()}_${
48✔
113
      this.lastSecuritySchemaIds[subject.type]
114
    }`;
115
  }
116

117
  public constructor({
118
    routing,
119
    config,
120
    title,
121
    version,
122
    serverUrl,
123
    successfulResponseDescription = "Successful response",
258✔
124
    errorResponseDescription = "Error response",
258✔
125
    hasSummaryFromDescription = true,
258✔
126
    composition = "inline",
240✔
127
    serializer = defaultSerializer,
258✔
128
  }: DocumentationParams) {
129
    super();
258✔
130
    this.addInfo({ title, version });
258✔
131
    for (const url of typeof serverUrl === "string" ? [serverUrl] : serverUrl) {
258!
132
      this.addServer({ url });
258✔
133
    }
134
    const onEndpoint: RoutingWalkerParams["onEndpoint"] = (
258✔
135
      endpoint,
136
      path,
137
      _method,
138
    ) => {
139
      const method = _method as Method;
366✔
140
      const commonParams = {
366✔
141
        path,
142
        method,
143
        endpoint,
144
        composition,
145
        serializer,
146
        getRef: this.getRef.bind(this),
147
        makeRef: this.makeRef.bind(this),
148
      };
149
      const [shortDesc, longDesc] = (["short", "long"] as const).map(
366✔
150
        endpoint.getDescription.bind(endpoint),
151
      );
152
      const inputSources =
153
        config.inputSources?.[method] || defaultInputSources[method];
366✔
154
      const depictedParams = depictRequestParams({
366✔
155
        ...commonParams,
156
        inputSources,
157
      });
158
      const operationId = this.ensureUniqOperationId(
366✔
159
        path,
160
        method,
161
        endpoint.getOperationId(method),
162
      );
163
      const operation: OperationObject = {
360✔
164
        operationId,
165
        responses: {
166
          [endpoint.getStatusCode("positive")]: depictResponse({
167
            ...commonParams,
168
            clue: successfulResponseDescription,
169
            isPositive: true,
170
          }),
171
          [endpoint.getStatusCode("negative")]: depictResponse({
172
            ...commonParams,
173
            clue: errorResponseDescription,
174
            isPositive: false,
175
          }),
176
        },
177
      };
178
      if (longDesc) {
360✔
179
        operation.description = longDesc;
72✔
180
        if (hasSummaryFromDescription && shortDesc === undefined) {
72✔
181
          operation.summary = ensureShortDescription(longDesc);
60✔
182
        }
183
      }
184
      if (shortDesc) {
360✔
185
        operation.summary = ensureShortDescription(shortDesc);
36✔
186
      }
187
      if (endpoint.getTags().length > 0) {
360✔
188
        operation.tags = endpoint.getTags();
84✔
189
      }
190
      if (depictedParams.length > 0) {
360✔
191
        operation.parameters = depictedParams;
126✔
192
      }
193
      if (inputSources.includes("body")) {
360✔
194
        operation.requestBody = depictRequest(commonParams);
198✔
195
      }
196
      const securityRefs = depictSecurityRefs(
318✔
197
        mapLogicalContainer(
198
          depictSecurity(endpoint.getSecurity()),
199
          (securitySchema) => {
200
            const name = this.ensureUniqSecuritySchemaName(securitySchema);
54✔
201
            const scopes = ["oauth2", "openIdConnect"].includes(
54✔
202
              securitySchema.type,
203
            )
204
              ? endpoint.getScopes()
205
              : [];
206
            this.addSecurityScheme(name, securitySchema);
54✔
207
            return { name, scopes };
54✔
208
          },
209
        ),
210
      );
211
      if (securityRefs.length > 0) {
318✔
212
        operation.security = securityRefs;
30✔
213
      }
214
      const swaggerCompatiblePath = reformatParamsInPath(path);
318✔
215
      this.addPath(swaggerCompatiblePath, {
318✔
216
        [method]: operation,
217
      });
218
    };
219
    walkRouting({ routing, onEndpoint });
258✔
220
    this.rootDoc.tags = config.tags ? depictTags(config.tags) : [];
210✔
221
  }
222
}
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