Skip to content

Commit 11fb65b

Browse files
feat: Update OpenAPI class to support adding multiple endpoints and enhance endpoint builder functionality
1 parent 51fd737 commit 11fb65b

6 files changed

Lines changed: 214 additions & 18 deletions

File tree

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@murat/openapi",
33
"exports": "./mod.ts",
4-
"version": "0.1.7",
4+
"version": "0.1.8",
55
"tasks": {
66
"lint": "deno lint",
77
"test": "deno test --allow-all",

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@murat/openapi",
3-
"version": "0.1.7",
3+
"version": "0.1.8",
44
"license": "MIT",
55
"exports": "./mod.ts",
66
"imports": {

src/Core.ts

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,71 @@
11
import type {
22
OpenAPICore,
3+
OpenAPIPathItem,
34
OpenAPISecurityRequirement,
45
OpenAPITag,
5-
} from "./Core.types.ts";
6-
import { createEndpointBuilder } from "./EndpointBuilder.ts";
7-
import { createEndpointPath, type EndpointPath } from "./EndpointPath.ts";
8-
import type { AllowedLicenses } from "./Licenses.types.ts";
6+
} from './Core.types.ts';
7+
import { createEndpointBuilder, EndpointBuilder } from './EndpointBuilder.ts';
8+
import { createEndpointPath, type EndpointPath } from './EndpointPath.ts';
9+
import type { AllowedLicenses } from './Licenses.types.ts';
910

1011
class OpenAPI {
1112
private raw: OpenAPICore;
13+
private endpoints: EndpointBuilder[];
1214

1315
constructor() {
1416
this.raw = {
15-
openapi: "3.1.0",
17+
openapi: '3.1.0',
1618
info: {
17-
title: "OpenAPI 3.1.0",
18-
version: "1.0.0",
19+
title: 'OpenAPI 3.1.0',
20+
version: '1.0.0',
1921
},
2022
};
23+
this.endpoints = [];
2124
}
2225

2326
getJSON(): OpenAPICore {
24-
return this.raw;
27+
// Create a deep copy of the raw object
28+
const result = JSON.parse(JSON.stringify(this.raw)) as OpenAPICore;
29+
30+
// Process and group endpoints by path
31+
if (this.endpoints.length > 0) {
32+
if (!result.paths) {
33+
result.paths = {};
34+
}
35+
36+
for (const endpoint of this.endpoints) {
37+
if (!endpoint.path || !endpoint.method) {
38+
console.warn(
39+
'Endpoint is missing path or method, skipping',
40+
endpoint
41+
);
42+
continue;
43+
}
44+
45+
if (!result.paths[endpoint.path]) {
46+
result.paths[endpoint.path] = {} as OpenAPIPathItem;
47+
}
48+
49+
const pathItem = result.paths[endpoint.path] as OpenAPIPathItem;
50+
pathItem[endpoint.method as keyof OpenAPIPathItem] =
51+
// deno-lint-ignore no-explicit-any
52+
endpoint.operation as any;
53+
}
54+
}
55+
56+
return result;
57+
}
58+
59+
// Add an array of endpoints
60+
addEndpoints(endpoints: EndpointBuilder[]): this {
61+
this.endpoints.push(...endpoints);
62+
return this;
63+
}
64+
65+
// Add a single endpoint
66+
addEndpoint(endpoint: EndpointBuilder): this {
67+
this.endpoints.push(endpoint);
68+
return this;
2569
}
2670

2771
setTitle(title: string): this {
@@ -63,19 +107,19 @@ class OpenAPI {
63107
}
64108

65109
setLicenseName(name: string): this {
66-
this.raw.info.license = this.raw.info.license || { name: "" };
110+
this.raw.info.license = this.raw.info.license || { name: '' };
67111
this.raw.info.license.name = name;
68112
return this;
69113
}
70114

71115
setLicenseUrl(url: string): this {
72-
this.raw.info.license = this.raw.info.license || { name: "" };
116+
this.raw.info.license = this.raw.info.license || { name: '' };
73117
this.raw.info.license.url = url;
74118
return this;
75119
}
76120

77121
setLicenseIdentifier(identifier: AllowedLicenses): this {
78-
this.raw.info.license = this.raw.info.license || { name: "" };
122+
this.raw.info.license = this.raw.info.license || { name: '' };
79123
this.raw.info.license.identifier = identifier;
80124
return this;
81125
}
@@ -104,7 +148,7 @@ class OpenAPI {
104148
}
105149

106150
setSecurity(scheme: {
107-
type: "apiKey" | "http" | "oauth2" | "openIdConnect";
151+
type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';
108152
name: string;
109153
scopes?: string[];
110154
}): this {
@@ -157,6 +201,7 @@ export { createEndpointBuilder, createEndpointPath, OpenAPI };
157201

158202
// const openAPI = new OpenAPI();
159203

204+
// // OLD USAGE
160205
// openAPI
161206
// .setTitle('My API')
162207
// .setVersion('1.0.0')
@@ -233,3 +278,30 @@ export { createEndpointBuilder, createEndpointPath, OpenAPI };
233278
// })
234279
// )
235280
// );
281+
282+
// // NEW USAGE
283+
// const endpoint1 = createEndpointBuilder()
284+
// .setMethod('get')
285+
// .setPath('/tasks')
286+
// .setSummary('Get Tasks');
287+
288+
// const endpoint2 = createEndpointBuilder()
289+
// .setMethod('post')
290+
// .setPath('/tasks')
291+
// .setSummary('Create Task')
292+
// .setRequestBody(
293+
// {
294+
// 'application/json': {
295+
// schema: {
296+
// type: 'object',
297+
// properties: {
298+
// title: { type: 'string' },
299+
// completed: { type: 'boolean' },
300+
// },
301+
// },
302+
// },
303+
// },
304+
// true
305+
// );
306+
307+
// openAPI.addEndpoints([endpoint1, endpoint2]);

src/EndpointBuilder.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,25 @@ import type {
55

66
class EndpointBuilder {
77
method: string;
8+
path: string;
89
operation: OpenAPIOperation;
910

10-
constructor(method: string) {
11-
this.method = method;
11+
constructor(method?: string) {
12+
this.method = method || "";
13+
this.path = "";
1214
this.operation = {} as OpenAPIOperation;
1315
}
1416

17+
setMethod(method: string): this {
18+
this.method = method.toLowerCase();
19+
return this;
20+
}
21+
22+
setPath(path: string): this {
23+
this.path = path;
24+
return this;
25+
}
26+
1527
setOperationId(operationId: string): this {
1628
this.operation.operationId = operationId;
1729
return this;
@@ -106,7 +118,7 @@ class EndpointBuilder {
106118
}
107119

108120
function createEndpointBuilder(
109-
method: string,
121+
method?: string,
110122
): EndpointBuilder {
111123
return new EndpointBuilder(method);
112124
}

src/EndpointPath.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class EndpointPath {
5353
);
5454
}
5555

56-
// deno-lint-ignore no-explicit-any
5756
this.pathItem[builder.method as keyof OpenAPIPathItem] = builder
57+
// deno-lint-ignore no-explicit-any
5858
.operation as any;
5959
return this;
6060
}

test/new_usage_test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { createEndpointBuilder, OpenAPI } from "../src/Core.ts";
2+
import { assertEquals } from "@std/assert";
3+
4+
Deno.test("New usage with individual endpoints", () => {
5+
const api = new OpenAPI()
6+
.setTitle("Tasks API")
7+
.setVersion("1.0.0");
8+
9+
const endpoint1 = createEndpointBuilder()
10+
.setMethod('get')
11+
.setPath('/tasks')
12+
.setSummary('Get Tasks')
13+
.setOperationId('getTasks')
14+
.setResponses({
15+
200: {
16+
description: "Success response",
17+
content: {
18+
"application/json": {
19+
schema: {
20+
type: "array",
21+
items: {
22+
type: "object",
23+
properties: {
24+
id: { type: "string" },
25+
title: { type: "string" },
26+
completed: { type: "boolean" },
27+
},
28+
},
29+
},
30+
},
31+
},
32+
},
33+
});
34+
35+
const endpoint2 = createEndpointBuilder()
36+
.setMethod('post')
37+
.setPath('/tasks')
38+
.setSummary('Create Task')
39+
.setOperationId('createTask')
40+
.setRequestBody(
41+
{
42+
'application/json': {
43+
schema: {
44+
type: 'object',
45+
properties: {
46+
title: { type: 'string' },
47+
completed: { type: 'boolean' },
48+
},
49+
},
50+
},
51+
},
52+
true
53+
)
54+
.setResponses({
55+
201: {
56+
description: "Task created",
57+
},
58+
});
59+
60+
// Add endpoint with different path
61+
const endpoint3 = createEndpointBuilder()
62+
.setMethod('get')
63+
.setPath('/tasks/{taskId}')
64+
.setSummary('Get Task by ID')
65+
.setOperationId('getTaskById')
66+
.setParameter('taskId', 'path', true, 'Task identifier')
67+
.setResponses({
68+
200: {
69+
description: "Task details",
70+
},
71+
404: {
72+
description: "Task not found",
73+
},
74+
});
75+
76+
api.addEndpoints([endpoint1, endpoint2, endpoint3]);
77+
78+
const result = api.getJSON();
79+
80+
// Verify that we have two distinct paths
81+
assertEquals(Object.keys(result.paths || {}).length, 2);
82+
83+
// Check that /tasks has both GET and POST methods
84+
// deno-lint-ignore no-explicit-any
85+
const tasksPath = result.paths?.['/tasks'] as any;
86+
assertEquals(typeof tasksPath?.get, 'object');
87+
assertEquals(typeof tasksPath?.post, 'object');
88+
assertEquals(tasksPath?.get?.operationId, 'getTasks');
89+
assertEquals(tasksPath?.post?.operationId, 'createTask');
90+
91+
// Check that /tasks/{taskId} has GET method
92+
// deno-lint-ignore no-explicit-any
93+
const taskByIdPath = result.paths?.['/tasks/{taskId}'] as any;
94+
assertEquals(typeof taskByIdPath?.get, 'object');
95+
assertEquals(taskByIdPath?.get?.operationId, 'getTaskById');
96+
});
97+
98+
Deno.test("Adding a single endpoint works", () => {
99+
const api = new OpenAPI();
100+
101+
api.addEndpoint(
102+
createEndpointBuilder()
103+
.setMethod('get')
104+
.setPath('/users')
105+
.setOperationId('getUsers')
106+
);
107+
108+
// deno-lint-ignore no-explicit-any
109+
const result = api.getJSON() as any;
110+
assertEquals(typeof result.paths?.['/users']?.get, 'object');
111+
assertEquals(result.paths?.['/users']?.get?.operationId, 'getUsers');
112+
});

0 commit comments

Comments
 (0)