Skip to content

Commit 077512c

Browse files
committed
feat: rootPath option
1 parent c9a7eac commit 077512c

File tree

2 files changed

+182
-3
lines changed

2 files changed

+182
-3
lines changed

src/core/generateOpenApiSpec.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,179 @@ describe("generateOpenApiSpec", () => {
205205

206206
readFileSpy.mockRestore();
207207
});
208+
209+
it("should handle rootPath option correctly", async () => {
210+
const repoName = "omermecitoglu/example-user-service";
211+
const branchName = "main";
212+
const filePath = "src/app/users/route.ts";
213+
const response = await fetch(`https://raw.githubusercontent.com/${repoName}/refs/heads/${branchName}/${filePath}`);
214+
const example = await response.text();
215+
216+
jest.spyOn(next, "findAppFolderPath").mockResolvedValueOnce("/app");
217+
(getDirectoryItems as jest.Mock<typeof getDirectoryItems>).mockResolvedValue([
218+
"/app/api/v1/test/route.ts",
219+
"/app/api/v1/users/route.ts",
220+
]);
221+
(filterDirectoryItems as jest.Mock<typeof filterDirectoryItems>).mockReturnValue([
222+
"/app/api/v1/test/route.ts",
223+
"/app/api/v1/users/route.ts",
224+
]);
225+
const readFileSpy = jest.spyOn(fs, "readFile").mockImplementation(routePath => {
226+
switch (routePath) {
227+
case "/app/api/v1/test/route.ts":
228+
return Promise.resolve(example);
229+
case "/app/api/v1/users/route.ts":
230+
return Promise.resolve("export async function GET(request: Request) {}");
231+
default:
232+
throw new Error("Unexpected route path");
233+
}
234+
// do nothing
235+
});
236+
(getPackageMetadata as jest.Mock).mockReturnValue({
237+
serviceName: "Test Service",
238+
version: "1.0.0",
239+
});
240+
241+
const result = await generateOpenApiSpec(schemas, { rootPath: "/api/v1" });
242+
243+
expect(result).toEqual({
244+
openapi: "3.1.0",
245+
info: {
246+
title: "Test Service",
247+
version: "1.0.0",
248+
},
249+
paths: {
250+
"/test": {
251+
get: {
252+
summary: "Get all users",
253+
description: "Retrieve a list of users",
254+
operationId: "getUsers",
255+
parameters: [
256+
{
257+
description: "List of the column names",
258+
in: "query",
259+
name: "select",
260+
required: false,
261+
schema: {
262+
default: [],
263+
description: "List of the column names",
264+
items: {
265+
enum: [
266+
"id",
267+
"name",
268+
],
269+
type: "string",
270+
},
271+
type: "array",
272+
},
273+
},
274+
],
275+
requestBody: undefined,
276+
responses: {
277+
200: {
278+
content: {
279+
"application/json": {
280+
schema: {
281+
items: {
282+
$ref: "#/components/schemas/UserDTO",
283+
},
284+
type: "array",
285+
},
286+
},
287+
},
288+
description: "Returns a list of users",
289+
},
290+
400: {
291+
content: undefined,
292+
description: "Bad Request",
293+
},
294+
500: {
295+
content: undefined,
296+
description: "Internal Server Error",
297+
},
298+
},
299+
tags: [
300+
"Users",
301+
],
302+
},
303+
post: {
304+
description: "Create a new user",
305+
operationId: "createUser",
306+
parameters: undefined,
307+
requestBody: {
308+
content: {
309+
"application/json": {
310+
schema: {
311+
$ref: "#/components/schemas/NewUserDTO",
312+
},
313+
},
314+
},
315+
required: true,
316+
},
317+
responses: {
318+
201: {
319+
content: {
320+
"application/json": {
321+
schema: {
322+
$ref: "#/components/schemas/UserDTO",
323+
},
324+
},
325+
},
326+
description: "User created successfully",
327+
},
328+
400: {
329+
content: undefined,
330+
description: "Bad Request",
331+
},
332+
409: {
333+
content: undefined,
334+
description: "Email already exists",
335+
},
336+
500: {
337+
338+
description: "Internal Server Error",
339+
},
340+
},
341+
summary: "Create user",
342+
tags: [
343+
"Users",
344+
],
345+
},
346+
},
347+
},
348+
components: {
349+
schemas: {
350+
UserDTO: {
351+
type: "object",
352+
properties: {
353+
id: {
354+
type: "string",
355+
},
356+
name: {
357+
type: "string",
358+
},
359+
},
360+
additionalProperties: false,
361+
required: ["id", "name"],
362+
},
363+
NewUserDTO: {
364+
type: "object",
365+
properties: {
366+
id: {
367+
type: "string",
368+
},
369+
name: {
370+
type: "string",
371+
},
372+
},
373+
additionalProperties: false,
374+
required: ["name"],
375+
},
376+
},
377+
},
378+
tags: [],
379+
});
380+
381+
readFileSpy.mockRestore();
382+
});
208383
});

src/core/generateOpenApiSpec.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from "node:path";
12
import getPackageMetadata from "@omer-x/package-metadata";
23
import { filterDirectoryItems, getDirectoryItems } from "./dir";
34
import isDocumentedRoute from "./isDocumentedRoute";
@@ -12,18 +13,21 @@ type GeneratorOptions = {
1213
include?: string[],
1314
exclude?: string[],
1415
routeDefinerName?: string,
16+
rootPath?: string,
1517
};
1618

1719
export default async function generateOpenApiSpec(schemas: Record<string, ZodType>, {
1820
include: includeOption = [],
1921
exclude: excludeOption = [],
2022
routeDefinerName = "defineRoute",
23+
rootPath: additionalRootPath,
2124
}: GeneratorOptions = {}) {
2225
const verifiedOptions = verifyOptions(includeOption, excludeOption);
2326
const appFolderPath = await findAppFolderPath();
2427
if (!appFolderPath) throw new Error("This is not a Next.js application!");
25-
const routes = await getDirectoryItems(appFolderPath, "route.ts");
26-
const verifiedRoutes = filterDirectoryItems(appFolderPath, routes, verifiedOptions.include, verifiedOptions.exclude);
28+
const rootPath = additionalRootPath ? path.resolve(appFolderPath, "./" + additionalRootPath) : appFolderPath;
29+
const routes = await getDirectoryItems(rootPath, "route.ts");
30+
const verifiedRoutes = filterDirectoryItems(rootPath, routes, verifiedOptions.include, verifiedOptions.exclude);
2731
const validRoutes: RouteRecord[] = [];
2832
for (const route of verifiedRoutes) {
2933
const isDocumented = await isDocumentedRoute(route);
@@ -34,7 +38,7 @@ export default async function generateOpenApiSpec(schemas: Record<string, ZodTyp
3438
validRoutes.push(createRouteRecord(
3539
method.toLocaleLowerCase(),
3640
route,
37-
appFolderPath,
41+
rootPath,
3842
routeHandler.apiData,
3943
));
4044
}

0 commit comments

Comments
 (0)