@cinnabun/openapi 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/openapi-generator.test.ts +194 -0
- package/__tests__/openapi-module.test.ts +115 -0
- package/__tests__/route-scanner.test.ts +120 -0
- package/__tests__/schema-converter.test.ts +73 -0
- package/dist/decorators/api-operation.d.ts +2 -0
- package/dist/decorators/api-operation.js +6 -0
- package/dist/decorators/api-property.d.ts +2 -0
- package/dist/decorators/api-property.js +6 -0
- package/dist/decorators/api-response.d.ts +4 -0
- package/dist/decorators/api-response.js +6 -0
- package/dist/decorators/api-tags.d.ts +1 -0
- package/dist/decorators/api-tags.js +6 -0
- package/dist/generator/openapi-generator.d.ts +25 -0
- package/dist/generator/openapi-generator.js +166 -0
- package/dist/generator/route-scanner.d.ts +19 -0
- package/dist/generator/route-scanner.js +40 -0
- package/dist/generator/schema-converter.d.ts +5 -0
- package/dist/generator/schema-converter.js +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +9 -0
- package/dist/interfaces/openapi-options.d.ts +17 -0
- package/dist/interfaces/openapi-options.js +1 -0
- package/dist/metadata/openapi-storage.d.ts +40 -0
- package/dist/metadata/openapi-storage.js +51 -0
- package/dist/openapi.module.d.ts +6 -0
- package/dist/openapi.module.js +103 -0
- package/package.json +27 -0
- package/src/decorators/api-operation.ts +16 -0
- package/src/decorators/api-property.ts +16 -0
- package/src/decorators/api-response.ts +14 -0
- package/src/decorators/api-tags.ts +7 -0
- package/src/generator/openapi-generator.ts +213 -0
- package/src/generator/route-scanner.ts +92 -0
- package/src/generator/schema-converter.ts +21 -0
- package/src/index.ts +14 -0
- package/src/interfaces/openapi-options.ts +11 -0
- package/src/metadata/openapi-storage.ts +108 -0
- package/src/openapi.module.ts +91 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
RestController,
|
|
5
|
+
GetMapping,
|
|
6
|
+
PostMapping,
|
|
7
|
+
Param,
|
|
8
|
+
HttpCode,
|
|
9
|
+
metadataStorage,
|
|
10
|
+
} from "@cinnabun/core";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { Validate } from "@cinnabun/core";
|
|
13
|
+
import { ApiTags, ApiOperation, ApiResponse } from "../src/index.js";
|
|
14
|
+
import { scanRoutes } from "../src/generator/route-scanner.js";
|
|
15
|
+
import { generateSpec } from "../src/generator/openapi-generator.js";
|
|
16
|
+
import { openApiMetadataStorage } from "../src/metadata/openapi-storage.js";
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
openApiMetadataStorage.reset();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("openapi-generator", () => {
|
|
23
|
+
it("generates valid OpenAPI 3.1 spec structure", () => {
|
|
24
|
+
@RestController("/api/v1")
|
|
25
|
+
@ApiTags("Users")
|
|
26
|
+
class OpenApiSpecUserController {
|
|
27
|
+
@GetMapping("/")
|
|
28
|
+
@ApiOperation({ summary: "List users" })
|
|
29
|
+
@ApiResponse(200, { description: "List of users" })
|
|
30
|
+
findAll() {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const routes = scanRoutes();
|
|
36
|
+
const options = {
|
|
37
|
+
title: "Test API",
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const spec = generateSpec(options, routes);
|
|
42
|
+
|
|
43
|
+
expect(spec.openapi).toBe("3.1.0");
|
|
44
|
+
expect(spec.info).toEqual({
|
|
45
|
+
title: "Test API",
|
|
46
|
+
version: "1.0.0",
|
|
47
|
+
});
|
|
48
|
+
expect(spec.paths).toBeDefined();
|
|
49
|
+
expect(typeof spec.paths).toBe("object");
|
|
50
|
+
expect(spec.components).toBeDefined();
|
|
51
|
+
expect(spec.components.schemas).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("generates paths from route registrations", () => {
|
|
55
|
+
@RestController("/items")
|
|
56
|
+
class ItemController {
|
|
57
|
+
@GetMapping("/")
|
|
58
|
+
list() {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@GetMapping("/:id")
|
|
63
|
+
get(@Param("id") id: string) {
|
|
64
|
+
return { id };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@PostMapping("/")
|
|
68
|
+
@HttpCode(201)
|
|
69
|
+
create() {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const routes = scanRoutes();
|
|
75
|
+
const spec = generateSpec(
|
|
76
|
+
{ title: "API", version: "1.0.0" },
|
|
77
|
+
routes,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(spec.paths["/items"]).toBeDefined();
|
|
81
|
+
expect(spec.paths["/items"].get).toBeDefined();
|
|
82
|
+
expect(spec.paths["/items"].post).toBeDefined();
|
|
83
|
+
|
|
84
|
+
expect(spec.paths["/items/{id}"]).toBeDefined();
|
|
85
|
+
expect(spec.paths["/items/{id}"].get).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("generates parameters from @Param and @Query", () => {
|
|
89
|
+
@RestController("/openapi-users")
|
|
90
|
+
class OpenApiParamUserController {
|
|
91
|
+
@GetMapping("/:id")
|
|
92
|
+
get(@Param("id") id: string) {
|
|
93
|
+
return { id };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const routes = scanRoutes();
|
|
98
|
+
const spec = generateSpec(
|
|
99
|
+
{ title: "API", version: "1.0.0" },
|
|
100
|
+
routes,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const getOp = spec.paths["/openapi-users/{id}"]?.get;
|
|
104
|
+
expect(getOp).toBeDefined();
|
|
105
|
+
expect(getOp!.parameters).toBeDefined();
|
|
106
|
+
expect(getOp!.parameters).toHaveLength(1);
|
|
107
|
+
expect(getOp!.parameters![0]).toMatchObject({
|
|
108
|
+
name: "id",
|
|
109
|
+
in: "path",
|
|
110
|
+
required: true,
|
|
111
|
+
schema: { type: "string" },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("generates requestBody from @Validate body schema", () => {
|
|
116
|
+
const CreateUserSchema = z.object({
|
|
117
|
+
name: z.string(),
|
|
118
|
+
email: z.string().email(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
@RestController("/openapi-create-users")
|
|
122
|
+
class OpenApiCreateUserController {
|
|
123
|
+
@PostMapping("/")
|
|
124
|
+
@Validate({ body: CreateUserSchema })
|
|
125
|
+
create() {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const routes = scanRoutes();
|
|
131
|
+
const spec = generateSpec(
|
|
132
|
+
{ title: "API", version: "1.0.0" },
|
|
133
|
+
routes,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const postOp = spec.paths["/openapi-create-users"]?.post;
|
|
137
|
+
expect(postOp).toBeDefined();
|
|
138
|
+
expect(postOp!.requestBody).toBeDefined();
|
|
139
|
+
expect(postOp!.requestBody!.required).toBe(true);
|
|
140
|
+
expect(postOp!.requestBody!.content).toBeDefined();
|
|
141
|
+
expect(postOp!.requestBody!.content!["application/json"]).toBeDefined();
|
|
142
|
+
expect(postOp!.requestBody!.content!["application/json"].schema).toHaveProperty("$ref");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("generates responses from @ApiResponse and @HttpCode", () => {
|
|
146
|
+
@RestController("/openapi-response-users")
|
|
147
|
+
class OpenApiResponseUserController {
|
|
148
|
+
@GetMapping("/")
|
|
149
|
+
@ApiResponse(200, { description: "List of users" })
|
|
150
|
+
list() {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@PostMapping("/")
|
|
155
|
+
@HttpCode(201)
|
|
156
|
+
create() {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const routes = scanRoutes();
|
|
162
|
+
const spec = generateSpec(
|
|
163
|
+
{ title: "API", version: "1.0.0" },
|
|
164
|
+
routes,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const getOp = spec.paths["/openapi-response-users"]?.get;
|
|
168
|
+
expect(getOp!.responses["200"]).toBeDefined();
|
|
169
|
+
expect(getOp!.responses["200"].description).toBe("List of users");
|
|
170
|
+
|
|
171
|
+
const postOp = spec.paths["/openapi-response-users"]?.post;
|
|
172
|
+
expect(postOp!.responses["201"]).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("adds bearer auth security scheme when bearerAuth is true", () => {
|
|
176
|
+
const routes = scanRoutes();
|
|
177
|
+
const spec = generateSpec(
|
|
178
|
+
{
|
|
179
|
+
title: "API",
|
|
180
|
+
version: "1.0.0",
|
|
181
|
+
bearerAuth: true,
|
|
182
|
+
},
|
|
183
|
+
routes,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(spec.components.securitySchemes).toBeDefined();
|
|
187
|
+
expect(spec.components.securitySchemes!.bearerAuth).toEqual({
|
|
188
|
+
type: "http",
|
|
189
|
+
scheme: "bearer",
|
|
190
|
+
bearerFormat: "JWT",
|
|
191
|
+
});
|
|
192
|
+
expect(spec.security).toEqual([{ bearerAuth: [] }]);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
CinnabunApplication,
|
|
5
|
+
CinnabunApp,
|
|
6
|
+
CinnabunFactory,
|
|
7
|
+
RestController,
|
|
8
|
+
GetMapping,
|
|
9
|
+
} from "@cinnabun/core";
|
|
10
|
+
import { OpenApiModule } from "../src/openapi.module.js";
|
|
11
|
+
|
|
12
|
+
let app: CinnabunApplication | null = null;
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
if (app) {
|
|
16
|
+
await app.close();
|
|
17
|
+
app = null;
|
|
18
|
+
}
|
|
19
|
+
OpenApiModule.resetCache?.();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function url(path: string): string {
|
|
23
|
+
return `http://localhost:${app!.getPort()}${path}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("OpenApiModule", () => {
|
|
27
|
+
it("serves Swagger UI HTML at docs path", async () => {
|
|
28
|
+
const OpenApiMod = OpenApiModule.forRoot({
|
|
29
|
+
title: "Test API",
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
docsPath: "/api-docs",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
@CinnabunApp({ port: 0, scanPaths: [], imports: [OpenApiMod] })
|
|
35
|
+
@RestController("/api")
|
|
36
|
+
class App {
|
|
37
|
+
@GetMapping("/")
|
|
38
|
+
index() {
|
|
39
|
+
return { message: "ok" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
app = await CinnabunFactory.run(App);
|
|
44
|
+
|
|
45
|
+
const res = await fetch(url("/api-docs"));
|
|
46
|
+
expect(res.status).toBe(200);
|
|
47
|
+
|
|
48
|
+
const html = await res.text();
|
|
49
|
+
expect(html).toContain("<!DOCTYPE html>");
|
|
50
|
+
expect(html).toContain("swagger-ui");
|
|
51
|
+
expect(html).toContain("Test API");
|
|
52
|
+
expect(res.headers.get("content-type")).toContain("text/html");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("serves OpenAPI JSON spec at json path", async () => {
|
|
56
|
+
const OpenApiMod = OpenApiModule.forRoot({
|
|
57
|
+
title: "Test API",
|
|
58
|
+
version: "1.0.0",
|
|
59
|
+
docsPath: "/api-docs",
|
|
60
|
+
jsonPath: "/api-docs/json",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
@CinnabunApp({ port: 0, scanPaths: [], imports: [OpenApiMod] })
|
|
64
|
+
@RestController("/api")
|
|
65
|
+
class App {
|
|
66
|
+
@GetMapping("/")
|
|
67
|
+
index() {
|
|
68
|
+
return { message: "ok" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
app = await CinnabunFactory.run(App);
|
|
73
|
+
|
|
74
|
+
const res = await fetch(url("/api-docs/json"));
|
|
75
|
+
expect(res.status).toBe(200);
|
|
76
|
+
expect(res.headers.get("content-type")).toContain("application/json");
|
|
77
|
+
|
|
78
|
+
const spec = (await res.json()) as Record<string, unknown>;
|
|
79
|
+
expect(spec.openapi).toBe("3.1.0");
|
|
80
|
+
expect(spec.info).toEqual({
|
|
81
|
+
title: "Test API",
|
|
82
|
+
version: "1.0.0",
|
|
83
|
+
});
|
|
84
|
+
expect(spec.paths).toBeDefined();
|
|
85
|
+
expect(spec.paths!["/api"]).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("uses custom docs path when configured", async () => {
|
|
89
|
+
const OpenApiMod = OpenApiModule.forRoot({
|
|
90
|
+
title: "Custom API",
|
|
91
|
+
version: "2.0.0",
|
|
92
|
+
docsPath: "/docs",
|
|
93
|
+
jsonPath: "/docs/spec",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
@CinnabunApp({ port: 0, scanPaths: [], imports: [OpenApiMod] })
|
|
97
|
+
@RestController()
|
|
98
|
+
class App {
|
|
99
|
+
@GetMapping("/")
|
|
100
|
+
index() {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
app = await CinnabunFactory.run(App);
|
|
106
|
+
|
|
107
|
+
const docsRes = await fetch(url("/docs"));
|
|
108
|
+
expect(docsRes.status).toBe(200);
|
|
109
|
+
|
|
110
|
+
const jsonRes = await fetch(url("/docs/spec"));
|
|
111
|
+
expect(jsonRes.status).toBe(200);
|
|
112
|
+
const spec = (await jsonRes.json()) as Record<string, unknown>;
|
|
113
|
+
expect(spec.info?.title).toBe("Custom API");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import { describe, it, expect, afterEach } from "bun:test";
|
|
3
|
+
import {
|
|
4
|
+
RestController,
|
|
5
|
+
GetMapping,
|
|
6
|
+
PostMapping,
|
|
7
|
+
Param,
|
|
8
|
+
Query,
|
|
9
|
+
metadataStorage,
|
|
10
|
+
} from "@cinnabun/core";
|
|
11
|
+
import { ApiTags, ApiOperation, ApiResponse } from "../src/index.js";
|
|
12
|
+
import { scanRoutes } from "../src/generator/route-scanner.js";
|
|
13
|
+
import { openApiMetadataStorage } from "../src/metadata/openapi-storage.js";
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
openApiMetadataStorage.reset();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("route-scanner", () => {
|
|
20
|
+
it("produces correct path entries from registered routes", () => {
|
|
21
|
+
@RestController("/api/openapi-users")
|
|
22
|
+
@ApiTags("Users")
|
|
23
|
+
class OpenApiRouteUserController {
|
|
24
|
+
@GetMapping("/")
|
|
25
|
+
@ApiOperation({ summary: "List users" })
|
|
26
|
+
@ApiResponse(200, { description: "List of users" })
|
|
27
|
+
findAll() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@GetMapping("/:id")
|
|
32
|
+
@ApiOperation({ summary: "Get user by ID" })
|
|
33
|
+
findById(@Param("id") id: string) {
|
|
34
|
+
return { id };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@PostMapping("/")
|
|
38
|
+
create() {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const routes = scanRoutes();
|
|
44
|
+
const userRoutes = routes.filter(
|
|
45
|
+
(r) => r.controllerName === "OpenApiRouteUserController",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(userRoutes).toHaveLength(3);
|
|
49
|
+
|
|
50
|
+
const findAllRoute = userRoutes.find((r) => r.methodKey === "findAll");
|
|
51
|
+
expect(findAllRoute).toBeDefined();
|
|
52
|
+
expect(findAllRoute!.fullPath).toBe("/api/openapi-users");
|
|
53
|
+
expect(findAllRoute!.httpMethod).toBe("GET");
|
|
54
|
+
expect(findAllRoute!.tags).toEqual(["Users"]);
|
|
55
|
+
expect(findAllRoute!.operation?.summary).toBe("List users");
|
|
56
|
+
expect(findAllRoute!.responses).toHaveLength(1);
|
|
57
|
+
expect(findAllRoute!.responses[0].status).toBe(200);
|
|
58
|
+
|
|
59
|
+
const findByIdRoute = userRoutes.find((r) => r.methodKey === "findById");
|
|
60
|
+
expect(findByIdRoute).toBeDefined();
|
|
61
|
+
expect(findByIdRoute!.fullPath).toBe("/api/openapi-users/:id");
|
|
62
|
+
expect(findByIdRoute!.params).toHaveLength(1);
|
|
63
|
+
expect(findByIdRoute!.params[0].name).toBe("id");
|
|
64
|
+
expect(findByIdRoute!.params[0].source).toBe("param");
|
|
65
|
+
|
|
66
|
+
const createRoute = userRoutes.find((r) => r.methodKey === "create");
|
|
67
|
+
expect(createRoute).toBeDefined();
|
|
68
|
+
expect(createRoute!.httpMethod).toBe("POST");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("excludes specified controllers from scan", () => {
|
|
72
|
+
@RestController("/api/openapi-exclude-users")
|
|
73
|
+
class OpenApiExcludeUserController {
|
|
74
|
+
@GetMapping("/")
|
|
75
|
+
findAll() {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@RestController("/internal")
|
|
81
|
+
class InternalController {
|
|
82
|
+
@GetMapping("/health")
|
|
83
|
+
health() {
|
|
84
|
+
return { ok: true };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const routesExcludingInternal = scanRoutes([InternalController]);
|
|
89
|
+
const hasInternal = routesExcludingInternal.some(
|
|
90
|
+
(r) => r.controllerName === "InternalController",
|
|
91
|
+
);
|
|
92
|
+
expect(hasInternal).toBe(false);
|
|
93
|
+
|
|
94
|
+
const hasUsers = routesExcludingInternal.some(
|
|
95
|
+
(r) => r.controllerName === "OpenApiExcludeUserController",
|
|
96
|
+
);
|
|
97
|
+
expect(hasUsers).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("includes query params in scanned routes", () => {
|
|
101
|
+
@RestController("/search")
|
|
102
|
+
class SearchController {
|
|
103
|
+
@GetMapping("/")
|
|
104
|
+
search(@Query("q") q: string, @Query("limit") limit: string) {
|
|
105
|
+
return { q, limit };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const routes = scanRoutes();
|
|
110
|
+
const searchRoute = routes.find(
|
|
111
|
+
(r) => r.controllerName === "SearchController" && r.methodKey === "search",
|
|
112
|
+
);
|
|
113
|
+
expect(searchRoute).toBeDefined();
|
|
114
|
+
expect(searchRoute!.params).toHaveLength(2);
|
|
115
|
+
const queryParams = searchRoute!.params.filter((p) => p.source === "query");
|
|
116
|
+
expect(queryParams).toHaveLength(2);
|
|
117
|
+
expect(queryParams.map((p) => p.name)).toContain("q");
|
|
118
|
+
expect(queryParams.map((p) => p.name)).toContain("limit");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { convertZodSchema } from "../src/generator/schema-converter.js";
|
|
4
|
+
|
|
5
|
+
describe("schema-converter", () => {
|
|
6
|
+
it("converts string schema to JSON Schema", () => {
|
|
7
|
+
const schema = z.string();
|
|
8
|
+
const { jsonSchema, componentName } = convertZodSchema(schema, "StringSchema");
|
|
9
|
+
|
|
10
|
+
expect(componentName).toBe("StringSchema");
|
|
11
|
+
expect(jsonSchema.type).toBe("string");
|
|
12
|
+
expect(jsonSchema.$schema).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("converts number schema to JSON Schema", () => {
|
|
16
|
+
const schema = z.number();
|
|
17
|
+
const { jsonSchema } = convertZodSchema(schema, "NumberSchema");
|
|
18
|
+
|
|
19
|
+
expect(jsonSchema.type).toBe("number");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("converts boolean schema to JSON Schema", () => {
|
|
23
|
+
const schema = z.boolean();
|
|
24
|
+
const { jsonSchema } = convertZodSchema(schema, "BoolSchema");
|
|
25
|
+
|
|
26
|
+
expect(jsonSchema.type).toBe("boolean");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("converts object schema to JSON Schema", () => {
|
|
30
|
+
const schema = z.object({
|
|
31
|
+
name: z.string(),
|
|
32
|
+
age: z.number().optional(),
|
|
33
|
+
email: z.string().email(),
|
|
34
|
+
});
|
|
35
|
+
const { jsonSchema, componentName } = convertZodSchema(schema, "UserSchema");
|
|
36
|
+
|
|
37
|
+
expect(componentName).toBe("UserSchema");
|
|
38
|
+
expect(jsonSchema.type).toBe("object");
|
|
39
|
+
expect(jsonSchema.properties).toBeDefined();
|
|
40
|
+
expect(jsonSchema.properties).toHaveProperty("name");
|
|
41
|
+
expect(jsonSchema.properties).toHaveProperty("age");
|
|
42
|
+
expect(jsonSchema.properties).toHaveProperty("email");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("converts array schema to JSON Schema", () => {
|
|
46
|
+
const schema = z.array(z.string());
|
|
47
|
+
const { jsonSchema } = convertZodSchema(schema, "StringArray");
|
|
48
|
+
|
|
49
|
+
expect(jsonSchema.type).toBe("array");
|
|
50
|
+
expect(jsonSchema.items).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("converts enum schema to JSON Schema", () => {
|
|
54
|
+
const schema = z.enum(["a", "b", "c"]);
|
|
55
|
+
const { jsonSchema } = convertZodSchema(schema, "StatusEnum");
|
|
56
|
+
|
|
57
|
+
expect(jsonSchema.enum).toEqual(["a", "b", "c"]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("removes $schema from output for OpenAPI compatibility", () => {
|
|
61
|
+
const schema = z.object({ foo: z.string() });
|
|
62
|
+
const { jsonSchema } = convertZodSchema(schema);
|
|
63
|
+
|
|
64
|
+
expect(jsonSchema.$schema).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("uses default component name when not provided", () => {
|
|
68
|
+
const schema = z.string();
|
|
69
|
+
const { componentName } = convertZodSchema(schema);
|
|
70
|
+
|
|
71
|
+
expect(componentName).toBe("Schema");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ApiTags(...tags: string[]): ClassDecorator;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OpenApiModuleOptions } from "../interfaces/openapi-options.js";
|
|
2
|
+
import type { ScannedRoute } from "./route-scanner.js";
|
|
3
|
+
export interface OpenApiSpec {
|
|
4
|
+
openapi: string;
|
|
5
|
+
info: {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
servers?: {
|
|
11
|
+
url: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}[];
|
|
14
|
+
tags?: {
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}[];
|
|
18
|
+
paths: Record<string, Record<string, any>>;
|
|
19
|
+
components: {
|
|
20
|
+
schemas: Record<string, any>;
|
|
21
|
+
securitySchemes?: Record<string, any>;
|
|
22
|
+
};
|
|
23
|
+
security?: Record<string, string[]>[];
|
|
24
|
+
}
|
|
25
|
+
export declare function generateSpec(options: OpenApiModuleOptions, routes: ScannedRoute[]): OpenApiSpec;
|