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

RobinTail / express-zod-api / 4410939844

pending completion
4410939844

Pull #880

github

GitHub
Merge 1f79a241a into 1ca06dc0d
Pull Request #880: Bump @typescript-eslint/eslint-plugin from 5.54.1 to 5.55.0

499 of 524 branches covered (95.23%)

1119 of 1119 relevant lines covered (100.0%)

385.91 hits per line

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

97.59
/src/open-api.ts
1
import {
168✔
2
  OpenApiBuilder,
3
  OperationObject,
4
  ReferenceObject,
5
  SchemaObject,
6
  SecuritySchemeObject,
7
  SecuritySchemeType,
8
} from "openapi3-ts";
9
import { z } from "zod";
10
import { defaultInputSources, makeCleanId } from "./common-helpers";
168✔
11
import { CommonConfig } from "./config-type";
12
import { mapLogicalContainer } from "./logical-container";
168✔
13
import { Method } from "./method";
14
import {
168✔
15
  defaultSerializer,
16
  depictRequest,
17
  depictRequestParams,
18
  depictResponse,
19
  depictSecurity,
20
  depictSecurityRefs,
21
  depictTags,
22
  ensureShortDescription,
23
  reformatParamsInPath,
24
} from "./open-api-helpers";
25
import { Routing } from "./routing";
26
import { RoutingWalkerParams, walkRouting } from "./routing-walker";
168✔
27

28
interface GeneratorParams {
29
  title: string;
30
  version: string;
31
  serverUrl: string;
32
  routing: Routing;
33
  config: CommonConfig;
34
  /** @default Successful response */
35
  successfulResponseDescription?: string;
36
  /** @default Error response */
37
  errorResponseDescription?: string;
38
  /** @default true */
39
  hasSummaryFromDescription?: boolean;
40
  /** @default inline */
41
  composition?: "inline" | "components";
42
  /**
43
   * @desc Used for comparing schemas wrapped into z.lazy() to limit the recursion
44
   * @default JSON.stringify() + SHA1 hash as a hex digest
45
   * */
46
  serializer?: (schema: z.ZodTypeAny) => string;
47
}
48

49
export class OpenAPI extends OpenApiBuilder {
168✔
50
  protected lastSecuritySchemaIds: Partial<Record<SecuritySchemeType, number>> =
296✔
51
    {};
52
  protected lastOperationIdSuffixes: Record<string, number> = {};
296✔
53

54
  protected makeRef(
55
    name: string,
56
    schema: SchemaObject | ReferenceObject
57
  ): ReferenceObject {
58
    this.addSchema(name, schema);
176✔
59
    return { $ref: `#/components/schemas/${name}` };
176✔
60
  }
61

62
  protected hasRef(name: string): boolean {
63
    return name in (this.rootDoc.components?.schemas || {});
48!
64
  }
65

66
  protected ensureUniqOperationId(path: string, method: Method) {
67
    const operationId = makeCleanId(path, method);
392✔
68
    if (operationId in this.lastOperationIdSuffixes) {
392✔
69
      this.lastOperationIdSuffixes[operationId]++;
8✔
70
      return `${operationId}${this.lastOperationIdSuffixes[operationId]}`;
8✔
71
    }
72
    this.lastOperationIdSuffixes[operationId] = 1;
384✔
73
    return operationId;
384✔
74
  }
75

76
  protected ensureUniqSecuritySchemaName(subject: SecuritySchemeObject) {
77
    const serializedSubject = JSON.stringify(subject);
72✔
78
    for (const name in this.rootDoc.components?.securitySchemes || {}) {
72!
79
      if (
80✔
80
        serializedSubject ===
81
        JSON.stringify(this.rootDoc.components?.securitySchemes?.[name])
82
      ) {
83
        return name;
8✔
84
      }
85
    }
86
    this.lastSecuritySchemaIds[subject.type] =
64✔
87
      (this.lastSecuritySchemaIds?.[subject.type] || 0) + 1;
104✔
88
    return `${subject.type.toUpperCase()}_${
64✔
89
      this.lastSecuritySchemaIds[subject.type]
90
    }`;
91
  }
92

93
  public constructor({
94
    routing,
95
    config,
96
    title,
97
    version,
98
    serverUrl,
99
    successfulResponseDescription = "Successful response",
296✔
100
    errorResponseDescription = "Error response",
296✔
101
    hasSummaryFromDescription = true,
296✔
102
    composition = "inline",
280✔
103
    serializer = defaultSerializer,
296✔
104
  }: GeneratorParams) {
105
    super();
296✔
106
    this.addInfo({ title, version }).addServer({ url: serverUrl });
296✔
107
    const onEndpoint: RoutingWalkerParams["onEndpoint"] = (
296✔
108
      endpoint,
109
      path,
110
      _method
111
    ) => {
112
      const method = _method as Method;
392✔
113
      const commonParams = {
392✔
114
        path,
115
        method,
116
        endpoint,
117
        composition,
118
        serializer,
119
        hasRef: this.hasRef.bind(this),
120
        makeRef: this.makeRef.bind(this),
121
      };
122
      const [shortDesc, longDesc] = (["short", "long"] as const).map(
392✔
123
        endpoint.getDescription.bind(endpoint)
124
      );
125
      const inputSources =
126
        config.inputSources?.[method] || defaultInputSources[method];
392✔
127
      const depictedParams = depictRequestParams({
392✔
128
        ...commonParams,
129
        inputSources,
130
      });
131
      const operation: OperationObject = {
392✔
132
        operationId: this.ensureUniqOperationId(path, method),
133
        responses: {
134
          [endpoint.getStatusCode("positive")]: depictResponse({
135
            ...commonParams,
136
            clue: successfulResponseDescription,
137
            isPositive: true,
138
          }),
139
          [endpoint.getStatusCode("negative")]: depictResponse({
140
            ...commonParams,
141
            clue: errorResponseDescription,
142
            isPositive: false,
143
          }),
144
        },
145
      };
146
      if (longDesc) {
392✔
147
        operation.description = longDesc;
64✔
148
        if (hasSummaryFromDescription && shortDesc === undefined) {
64✔
149
          operation.summary = ensureShortDescription(longDesc);
48✔
150
        }
151
      }
152
      if (shortDesc) {
392✔
153
        operation.summary = ensureShortDescription(shortDesc);
48✔
154
      }
155
      if (endpoint.getTags().length > 0) {
392✔
156
        operation.tags = endpoint.getTags();
80✔
157
      }
158
      if (depictedParams.length > 0) {
392✔
159
        operation.parameters = depictedParams;
152✔
160
      }
161
      if (inputSources.includes("body")) {
392✔
162
        operation.requestBody = depictRequest(commonParams);
232✔
163
      }
164
      const securityRefs = depictSecurityRefs(
336✔
165
        mapLogicalContainer(
166
          depictSecurity(endpoint.getSecurity()),
167
          (securitySchema) => {
168
            const name = this.ensureUniqSecuritySchemaName(securitySchema);
72✔
169
            const scopes = ["oauth2", "openIdConnect"].includes(
72✔
170
              securitySchema.type
171
            )
172
              ? endpoint.getScopes()
173
              : [];
174
            this.addSecurityScheme(name, securitySchema);
72✔
175
            return { name, scopes };
72✔
176
          }
177
        )
178
      );
179
      if (securityRefs.length > 0) {
336✔
180
        operation.security = securityRefs;
40✔
181
      }
182
      const swaggerCompatiblePath = reformatParamsInPath(path);
336✔
183
      this.addPath(swaggerCompatiblePath, {
336✔
184
        [method]: operation,
185
      });
186
    };
187
    walkRouting({ routing, onEndpoint });
296✔
188
    this.rootDoc.tags = config.tags ? depictTags(config.tags) : [];
240✔
189
  }
190
}
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