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

RobinTail / express-zod-api / 8348850131

19 Mar 2024 07:14PM CUT coverage: 100.0%. Remained the same
8348850131

push

github

RobinTail
17.4.1-beta1

665 of 698 branches covered (95.27%)

1096 of 1096 relevant lines covered (100.0%)

601.58 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
  ReferenceObject,
5
  ResponsesObject,
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
    {};
322✔
72
  protected lastOperationIdSuffixes: Record<string, number> = {};
322✔
73

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

82
  protected getRef(name: string): ReferenceObject | undefined {
83
    return name in (this.rootDoc.components?.schemas || {})
322!
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) {
462✔
94
      assert(
35✔
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;
28✔
104
      return userDefinedOperationId;
28✔
105
    }
106
    const operationId = makeCleanId(method, path);
427✔
107
    if (operationId in this.lastOperationIdSuffixes) {
427✔
108
      this.lastOperationIdSuffixes[operationId]++;
7✔
109
      return `${operationId}${this.lastOperationIdSuffixes[operationId]}`;
7✔
110
    }
111
    this.lastOperationIdSuffixes[operationId] = 1;
420✔
112
    return operationId;
420✔
113
  }
114

115
  protected ensureUniqSecuritySchemaName(subject: SecuritySchemeObject) {
116
    const serializedSubject = JSON.stringify(subject);
63✔
117
    for (const name in this.rootDoc.components?.securitySchemes || {}) {
63!
118
      if (
70✔
119
        serializedSubject ===
120
        JSON.stringify(this.rootDoc.components?.securitySchemes?.[name])
121
      ) {
122
        return name;
7✔
123
      }
124
    }
125
    this.lastSecuritySchemaIds[subject.type] =
56✔
126
      (this.lastSecuritySchemaIds?.[subject.type] || 0) + 1;
91✔
127
    return `${subject.type.toUpperCase()}_${
56✔
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,
322✔
140
    composition = "inline",
287✔
141
    serializer = defaultSerializer,
322✔
142
  }: DocumentationParams) {
143
    super();
322✔
144
    this.addInfo({ title, version });
322✔
145
    for (const url of typeof serverUrl === "string" ? [serverUrl] : serverUrl) {
322!
146
      this.addServer({ url });
322✔
147
    }
148
    const onEndpoint: RoutingWalkerParams["onEndpoint"] = (
322✔
149
      endpoint,
150
      path,
151
      _method,
152
    ) => {
153
      const method = _method as Method;
462✔
154
      const commonParams = {
462✔
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, description] = (["short", "long"] as const).map(
462✔
164
        endpoint.getDescription.bind(endpoint),
165
      );
166
      const summary = shortDesc
462✔
167
        ? ensureShortDescription(shortDesc)
168
        : hasSummaryFromDescription && description
1,260✔
169
          ? ensureShortDescription(description)
170
          : undefined;
171
      const tags = endpoint.getTags();
462✔
172
      const inputSources =
173
        config.inputSources?.[method] || defaultInputSources[method];
462✔
174
      const operationId = this.ensureUniqOperationId(
462✔
175
        path,
176
        method,
177
        endpoint.getOperationId(method),
178
      );
179

180
      const depictedParams = depictRequestParams({
455✔
181
        ...commonParams,
182
        inputSources,
183
        schema: endpoint.getSchema("input"),
184
        description: descriptions?.requestParameter?.call(null, {
185
          method,
186
          path,
187
          operationId,
188
        }),
189
      });
190

191
      const responses: ResponsesObject = {};
455✔
192
      for (const variant of ["positive", "negative"] as const) {
455✔
193
        const apiResponses = endpoint.getResponses(variant);
910✔
194
        for (const { mimeTypes, schema, statusCodes } of apiResponses) {
910✔
195
          for (const statusCode of statusCodes) {
938✔
196
            responses[statusCode] = depictResponse({
966✔
197
              ...commonParams,
198
              variant,
199
              schema,
200
              mimeTypes,
201
              statusCode,
202
              hasMultipleStatusCodes:
203
                apiResponses.length > 1 || statusCodes.length > 1,
1,862✔
204
              description: descriptions?.[`${variant}Response`]?.call(null, {
205
                method,
206
                path,
207
                operationId,
208
                statusCode,
209
              }),
210
            });
211
          }
212
        }
213
      }
214

215
      const requestBody = inputSources.includes("body")
455✔
216
        ? depictRequest({
217
            ...commonParams,
218
            schema: endpoint.getSchema("input"),
219
            mimeTypes: endpoint.getMimeTypes("input"),
220
            description: descriptions?.requestBody?.call(null, {
221
              method,
222
              path,
223
              operationId,
224
            }),
225
          })
226
        : undefined;
227

228
      const securityRefs = depictSecurityRefs(
406✔
229
        mapLogicalContainer(
230
          depictSecurity(endpoint.getSecurity(), inputSources),
231
          (securitySchema) => {
232
            const name = this.ensureUniqSecuritySchemaName(securitySchema);
63✔
233
            const scopes = ["oauth2", "openIdConnect"].includes(
63✔
234
              securitySchema.type,
235
            )
236
              ? endpoint.getScopes()
237
              : [];
238
            this.addSecurityScheme(name, securitySchema);
63✔
239
            return { name, scopes };
63✔
240
          },
241
        ),
242
      );
243

244
      this.addPath(reformatParamsInPath(path), {
406✔
245
        [method]: {
246
          operationId,
247
          summary,
248
          description,
249
          tags: tags.length > 0 ? tags : undefined,
406✔
250
          parameters: depictedParams.length > 0 ? depictedParams : undefined,
406✔
251
          requestBody,
252
          security: securityRefs.length > 0 ? securityRefs : undefined,
406✔
253
          responses,
254
        },
255
      });
256
    };
257
    walkRouting({ routing, onEndpoint });
322✔
258
    this.rootDoc.tags = config.tags ? depictTags(config.tags) : [];
266✔
259
  }
260
}
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