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

RobinTail / express-zod-api / 7379556135

01 Jan 2024 07:43PM CUT coverage: 100.0%. Remained the same
7379556135

Pull #1437

github

web-flow
Merge e5c5cd95d into a9e15f4f1
Pull Request #1437: Bump @typescript-eslint/parser from 6.16.0 to 6.17.0

684 of 716 branches covered (0.0%)

1186 of 1186 relevant lines covered (100.0%)

448.9 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
  ResponsesObject,
7
  SchemaObject,
8
  SecuritySchemeObject,
9
  SecuritySchemeType,
10
} from "openapi3-ts/oas31";
11
import { z } from "zod";
12
import { DocumentationError } from "./errors";
13
import {
14
  defaultInputSources,
15
  defaultSerializer,
16
  makeCleanId,
17
} from "./common-helpers";
18
import { CommonConfig } from "./config-type";
19
import { mapLogicalContainer } from "./logical-container";
20
import { Method } from "./method";
21
import {
22
  depictRequest,
23
  depictRequestParams,
24
  depictResponse,
25
  depictSecurity,
26
  depictSecurityRefs,
27
  depictTags,
28
  ensureShortDescription,
29
  reformatParamsInPath,
30
} from "./documentation-helpers";
31
import { Routing } from "./routing";
32
import { RoutingWalkerParams, walkRouting } from "./routing-walker";
33

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

40
/** @desc user defined function that creates a component description from its properties */
41
type Descriptor = (
42
  props: Record<"method" | "path" | "operationId", string>,
43
) => string;
44

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

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

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

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

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

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

131
  public constructor({
132
    routing,
133
    config,
134
    title,
135
    version,
136
    serverUrl,
137
    descriptions,
138
    hasSummaryFromDescription = true,
270✔
139
    composition = "inline",
240✔
140
    serializer = defaultSerializer,
270✔
141
  }: DocumentationParams) {
142
    super();
270✔
143
    this.addInfo({ title, version });
270✔
144
    for (const url of typeof serverUrl === "string" ? [serverUrl] : serverUrl) {
270!
145
      this.addServer({ url });
270✔
146
    }
147
    const onEndpoint: RoutingWalkerParams["onEndpoint"] = (
270✔
148
      endpoint,
149
      path,
150
      _method,
151
    ) => {
152
      const method = _method as Method;
378✔
153
      const commonParams = {
378✔
154
        path,
155
        method,
156
        endpoint,
157
        composition,
158
        serializer,
159
        getRef: this.getRef.bind(this),
160
        makeRef: this.makeRef.bind(this),
161
      };
162
      const [shortDesc, longDesc] = (["short", "long"] as const).map(
378✔
163
        endpoint.getDescription.bind(endpoint),
164
      );
165
      const inputSources =
166
        config.inputSources?.[method] || defaultInputSources[method];
378✔
167
      const operationId = this.ensureUniqOperationId(
378✔
168
        path,
169
        method,
170
        endpoint.getOperationId(method),
171
      );
172
      const depictedParams = depictRequestParams({
372✔
173
        ...commonParams,
174
        inputSources,
175
        description: descriptions?.requestParameter?.call(null, {
176
          method,
177
          path,
178
          operationId,
179
        }),
180
      });
181
      const responses = (
182
        ["positive", "negative"] as const
372✔
183
      ).reduce<ResponsesObject>(
184
        (agg, variant) => ({
744✔
185
          ...agg,
186
          [endpoint.getStatusCode(variant)]: depictResponse({
187
            ...commonParams,
188
            variant,
189
            description: descriptions?.[`${variant}Response`]?.call(null, {
190
              method,
191
              path,
192
              operationId,
193
            }),
194
          }),
195
        }),
196
        {},
197
      );
198
      const operation: OperationObject = { operationId, responses };
372✔
199
      if (longDesc) {
372✔
200
        operation.description = longDesc;
72✔
201
        if (hasSummaryFromDescription && shortDesc === undefined) {
72✔
202
          operation.summary = ensureShortDescription(longDesc);
60✔
203
        }
204
      }
205
      if (shortDesc) {
372✔
206
        operation.summary = ensureShortDescription(shortDesc);
36✔
207
      }
208
      if (endpoint.getTags().length > 0) {
372✔
209
        operation.tags = endpoint.getTags();
84✔
210
      }
211
      if (depictedParams.length > 0) {
372✔
212
        operation.parameters = depictedParams;
138✔
213
      }
214
      if (inputSources.includes("body")) {
372✔
215
        operation.requestBody = depictRequest({
210✔
216
          ...commonParams,
217
          description: descriptions?.requestBody?.call(null, {
218
            method,
219
            path,
220
            operationId,
221
          }),
222
        });
223
      }
224
      const securityRefs = depictSecurityRefs(
330✔
225
        mapLogicalContainer(
226
          depictSecurity(endpoint.getSecurity(), inputSources),
227
          (securitySchema) => {
228
            const name = this.ensureUniqSecuritySchemaName(securitySchema);
54✔
229
            const scopes = ["oauth2", "openIdConnect"].includes(
54✔
230
              securitySchema.type,
231
            )
232
              ? endpoint.getScopes()
233
              : [];
234
            this.addSecurityScheme(name, securitySchema);
54✔
235
            return { name, scopes };
54✔
236
          },
237
        ),
238
      );
239
      if (securityRefs.length > 0) {
330✔
240
        operation.security = securityRefs;
30✔
241
      }
242
      this.addPath(reformatParamsInPath(path), { [method]: operation });
330✔
243
    };
244
    walkRouting({ routing, onEndpoint });
270✔
245
    this.rootDoc.tags = config.tags ? depictTags(config.tags) : [];
222✔
246
  }
247
}
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