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

RobinTail / express-zod-api / 5547099979

pending completion
5547099979

Pull #1052

github

web-flow
Merge ba5af486f into 3ac8ab616
Pull Request #1052: Build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.60.1 to 5.62.0

573 of 600 branches covered (95.5%)

1156 of 1156 relevant lines covered (100.0%)

485.49 hits per line

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

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

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

52
export class Documentation extends OpenApiBuilder {
320✔
53
  protected lastSecuritySchemaIds: Partial<Record<SecuritySchemeType, number>> =
78✔
54
    {};
234✔
55
  protected lastOperationIdSuffixes: Record<string, number> = {};
312✔
56

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

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

71
  protected ensureUniqOperationId(path: string, method: Method) {
72
    const operationId = makeCleanId(path, method);
408✔
73
    if (operationId in this.lastOperationIdSuffixes) {
408✔
74
      this.lastOperationIdSuffixes[operationId]++;
8✔
75
      return `${operationId}${this.lastOperationIdSuffixes[operationId]}`;
8✔
76
    }
77
    this.lastOperationIdSuffixes[operationId] = 1;
400✔
78
    return operationId;
400✔
79
  }
80

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

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