spring-api-scanner 0.1.0
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/PLAN.md +142 -0
- package/README.md +60 -0
- package/dist/args.d.ts +9 -0
- package/dist/args.js +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -0
- package/dist/openapi.d.ts +67 -0
- package/dist/openapi.js +94 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +25 -0
- package/dist/resolver.d.ts +17 -0
- package/dist/resolver.js +206 -0
- package/dist/scanner.d.ts +22 -0
- package/dist/scanner.js +321 -0
- package/dist/ui.d.ts +36 -0
- package/dist/ui.js +407 -0
- package/docs/plans/2026-02-07-complete-remaining-tasks.md +61 -0
- package/package.json +23 -0
- package/spring-api-scanner-0.1.0.tgz +0 -0
- package/src/args.ts +76 -0
- package/src/index.ts +81 -0
- package/src/openapi.ts +191 -0
- package/src/output.ts +36 -0
- package/src/resolver.ts +274 -0
- package/src/scanner.ts +409 -0
- package/src/ui.ts +454 -0
- package/tests/args.test.d.ts +1 -0
- package/tests/args.test.ts +45 -0
- package/tests/fixtures/sample-service/src/main/kotlin/demo/UserController.kt +34 -0
- package/tests/fixtures/sample-service/src/main/kotlin/demo/UserDtos.kt +22 -0
- package/tests/golden/openapi.sample-service.json +164 -0
- package/tests/integration.test.ts +98 -0
- package/tests/openapi.test.d.ts +1 -0
- package/tests/openapi.test.ts +127 -0
- package/tests/output.test.ts +55 -0
- package/tests/resolver.test.ts +98 -0
- package/tests/scanner.test.ts +138 -0
- package/tests/ui.test.ts +85 -0
- package/tsconfig.json +15 -0
package/PLAN.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Spring API Scanner Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Build a standalone CLI tool that scans a Spring Boot Kotlin codebase and outputs:
|
|
5
|
+
- OpenAPI 3.0 JSON (`openapi.json`)
|
|
6
|
+
- A clean, minimal, detailed UI for human-readable API browsing
|
|
7
|
+
|
|
8
|
+
No dependency changes are required in the target Spring service.
|
|
9
|
+
|
|
10
|
+
## One-Shot Usage
|
|
11
|
+
- Serve UI + OpenAPI:
|
|
12
|
+
- `npx spring-api-scanner /path/to/service --serve`
|
|
13
|
+
- Export static docs only:
|
|
14
|
+
- `npx spring-api-scanner /path/to/service --no-serve --output ./api-docs`
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
Extract and visualize per endpoint:
|
|
18
|
+
- Endpoint name (controller method name)
|
|
19
|
+
- HTTP method + full path
|
|
20
|
+
- Path variables
|
|
21
|
+
- Query params
|
|
22
|
+
- Request headers
|
|
23
|
+
- Request body schema
|
|
24
|
+
- Response body schema
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
- **CLI**: Node.js + TypeScript
|
|
28
|
+
- **Kotlin parsing**: `tree-sitter-kotlin`
|
|
29
|
+
- **Spec generation**: OpenAPI 3.0 builder
|
|
30
|
+
- **UI**: static SPA (search/filter/expandable details)
|
|
31
|
+
|
|
32
|
+
Pipeline:
|
|
33
|
+
1. Scan Kotlin files
|
|
34
|
+
2. Parse Spring annotations and signatures
|
|
35
|
+
3. Resolve DTO schemas
|
|
36
|
+
4. Generate OpenAPI JSON
|
|
37
|
+
5. Render UI data model
|
|
38
|
+
6. Serve or export static output
|
|
39
|
+
|
|
40
|
+
## Tasks
|
|
41
|
+
|
|
42
|
+
### 1) CLI Skeleton
|
|
43
|
+
- Implement command:
|
|
44
|
+
- `spring-api-scanner <projectPath> [--port] [--output] [--serve] [--title] [--version]`
|
|
45
|
+
- Validate input path and print scan summary.
|
|
46
|
+
|
|
47
|
+
Acceptance:
|
|
48
|
+
- CLI runs and produces placeholder output.
|
|
49
|
+
|
|
50
|
+
### 2) Endpoint Extraction (Spring Kotlin)
|
|
51
|
+
- Scan `src/main/kotlin/**/*.kt`.
|
|
52
|
+
- Parse:
|
|
53
|
+
- `@RestController`
|
|
54
|
+
- class-level `@RequestMapping`
|
|
55
|
+
- method mappings: `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`, `@RequestMapping(method=...)`
|
|
56
|
+
- Extract:
|
|
57
|
+
- method, path, operation name
|
|
58
|
+
- `@PathVariable`, `@RequestParam`, `@RequestHeader`, `@RequestBody`
|
|
59
|
+
- return type
|
|
60
|
+
|
|
61
|
+
Acceptance:
|
|
62
|
+
- Real fixture project produces accurate endpoint list.
|
|
63
|
+
|
|
64
|
+
### 3) DTO & Type Resolver
|
|
65
|
+
- Parse Kotlin `data class` definitions.
|
|
66
|
+
- Resolve nested DTOs recursively.
|
|
67
|
+
- Nullability (`?`) => optional fields.
|
|
68
|
+
- Unwrap wrappers (`ResponseEntity<T>`, `List<T>`, `Page<T>`).
|
|
69
|
+
- Type mapping examples:
|
|
70
|
+
- `String -> string`
|
|
71
|
+
- `Long -> integer (int64)`
|
|
72
|
+
- `Int -> integer (int32)`
|
|
73
|
+
- `Boolean -> boolean`
|
|
74
|
+
- `Double -> number (double)`
|
|
75
|
+
- `Instant -> string (date-time)`
|
|
76
|
+
|
|
77
|
+
Acceptance:
|
|
78
|
+
- Request/response schemas include concrete fields where resolvable.
|
|
79
|
+
|
|
80
|
+
### 4) OpenAPI 3.0 Generator
|
|
81
|
+
- Build `openapi.json` with:
|
|
82
|
+
- `openapi`, `info`, `paths`, `components.schemas`
|
|
83
|
+
- For each operation include:
|
|
84
|
+
- `operationId`, `tags`, `parameters`, `requestBody`, `responses`
|
|
85
|
+
- Emit header params as `in: header`.
|
|
86
|
+
|
|
87
|
+
Acceptance:
|
|
88
|
+
- Spec validates in Swagger Editor.
|
|
89
|
+
|
|
90
|
+
### 5) Human-Readable UI
|
|
91
|
+
- Clean minimal detailed UI with:
|
|
92
|
+
- Search by endpoint/path
|
|
93
|
+
- Filter by method/controller
|
|
94
|
+
- Expandable endpoint cards
|
|
95
|
+
- Sections: path vars, query params, headers, request body, response body
|
|
96
|
+
- Download OpenAPI JSON
|
|
97
|
+
- Copy curl
|
|
98
|
+
- Responsive for desktop/mobile.
|
|
99
|
+
|
|
100
|
+
Acceptance:
|
|
101
|
+
- No missing endpoint metadata for supported annotation patterns.
|
|
102
|
+
|
|
103
|
+
### 6) One-Shot Orchestration
|
|
104
|
+
- End-to-end command flow:
|
|
105
|
+
- scan -> resolve -> generate -> render -> serve/export
|
|
106
|
+
- Modes:
|
|
107
|
+
- `--serve`: host UI + `/openapi.json`
|
|
108
|
+
- `--no-serve --output`: write static output
|
|
109
|
+
|
|
110
|
+
Acceptance:
|
|
111
|
+
- One command consistently produces both machine + human outputs.
|
|
112
|
+
|
|
113
|
+
### 7) Edge Cases & Robustness
|
|
114
|
+
- Support annotation forms:
|
|
115
|
+
- positional value, `value=`, `path=`, arrays
|
|
116
|
+
- Handle unresolved DTOs as warnings (no crash).
|
|
117
|
+
- Skip non-controller files safely.
|
|
118
|
+
|
|
119
|
+
Acceptance:
|
|
120
|
+
- Tool is stable on mixed real-world projects.
|
|
121
|
+
|
|
122
|
+
### 8) Testing Strategy
|
|
123
|
+
- Unit tests:
|
|
124
|
+
- parser
|
|
125
|
+
- type mapping
|
|
126
|
+
- OpenAPI generation
|
|
127
|
+
- Integration tests with realistic Kotlin fixtures.
|
|
128
|
+
- Golden-file test for deterministic `openapi.json` output.
|
|
129
|
+
|
|
130
|
+
Acceptance:
|
|
131
|
+
- CI-friendly deterministic test suite passes.
|
|
132
|
+
|
|
133
|
+
## Deliverables
|
|
134
|
+
- `spring-api-scanner` CLI
|
|
135
|
+
- Generated `openapi.json`
|
|
136
|
+
- Generated/served API catalog UI
|
|
137
|
+
- README with quickstart and examples
|
|
138
|
+
|
|
139
|
+
## Definition of Done
|
|
140
|
+
- One command produces valid OpenAPI JSON and complete UI docs.
|
|
141
|
+
- Endpoint cards include: name, path vars, params, headers, request/response bodies.
|
|
142
|
+
- Works without any dependency/code changes in the target Spring service.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# spring-api-scanner
|
|
2
|
+
|
|
3
|
+
Scan a Spring Boot Kotlin service and generate:
|
|
4
|
+
|
|
5
|
+
- `openapi.json` (OpenAPI 3.0)
|
|
6
|
+
- A static API catalog UI (`index.html` + `ui-data.json`)
|
|
7
|
+
|
|
8
|
+
No code/dependency changes are required in the target Spring service.
|
|
9
|
+
|
|
10
|
+
## Quickstart
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install
|
|
14
|
+
npm run build
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Serve generated docs:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node dist/index.js /path/to/spring-service --serve --port 3000
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Export static docs only:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node dist/index.js /path/to/spring-service --no-serve --output ./api-docs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
spring-api-scanner <projectPath> [--port] [--output] [--serve] [--no-serve] [--title] [--version]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What It Extracts
|
|
36
|
+
|
|
37
|
+
- `@RestController`
|
|
38
|
+
- Class-level `@RequestMapping`
|
|
39
|
+
- Method mappings:
|
|
40
|
+
- `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`
|
|
41
|
+
- `@RequestMapping(method = ...)`
|
|
42
|
+
- Parameters:
|
|
43
|
+
- `@PathVariable`, `@RequestParam`, `@RequestHeader`, `@RequestBody`
|
|
44
|
+
- Return type and DTO schemas from Kotlin `data class`
|
|
45
|
+
|
|
46
|
+
## Naming Strategy Notes
|
|
47
|
+
|
|
48
|
+
Schema field names follow these rules:
|
|
49
|
+
|
|
50
|
+
- Default DTO naming: `snake_case`
|
|
51
|
+
- If class has `@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy::class)`, fields stay `camelCase`
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm test
|
|
57
|
+
npm run build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Integration + golden-file tests are under `tests/integration.test.ts` and `tests/golden/openapi.sample-service.json`.
|
package/dist/args.d.ts
ADDED
package/dist/args.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const DEFAULT_PORT = 3000;
|
|
2
|
+
const DEFAULT_OUTPUT = "./api-docs";
|
|
3
|
+
const DEFAULT_TITLE = "Spring API Docs";
|
|
4
|
+
const DEFAULT_VERSION = "1.0.0";
|
|
5
|
+
export function parseCliArgs(argv) {
|
|
6
|
+
if (argv.length === 0 || argv[0].startsWith("--")) {
|
|
7
|
+
throw new Error("A project path is required: spring-api-scanner <projectPath>");
|
|
8
|
+
}
|
|
9
|
+
const projectPath = argv[0];
|
|
10
|
+
const options = {
|
|
11
|
+
projectPath,
|
|
12
|
+
port: DEFAULT_PORT,
|
|
13
|
+
output: DEFAULT_OUTPUT,
|
|
14
|
+
serve: false,
|
|
15
|
+
title: DEFAULT_TITLE,
|
|
16
|
+
version: DEFAULT_VERSION
|
|
17
|
+
};
|
|
18
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
19
|
+
const token = argv[i];
|
|
20
|
+
switch (token) {
|
|
21
|
+
case "--serve":
|
|
22
|
+
options.serve = true;
|
|
23
|
+
break;
|
|
24
|
+
case "--no-serve":
|
|
25
|
+
options.serve = false;
|
|
26
|
+
break;
|
|
27
|
+
case "--port":
|
|
28
|
+
options.port = parseIntegerFlag("--port", argv[++i]);
|
|
29
|
+
break;
|
|
30
|
+
case "--output":
|
|
31
|
+
options.output = parseStringFlag("--output", argv[++i]);
|
|
32
|
+
break;
|
|
33
|
+
case "--title":
|
|
34
|
+
options.title = parseStringFlag("--title", argv[++i]);
|
|
35
|
+
break;
|
|
36
|
+
case "--version":
|
|
37
|
+
options.version = parseStringFlag("--version", argv[++i]);
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Unknown argument: ${token}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
function parseStringFlag(flagName, rawValue) {
|
|
46
|
+
if (!rawValue || rawValue.startsWith("--")) {
|
|
47
|
+
throw new Error(`Missing value for ${flagName}`);
|
|
48
|
+
}
|
|
49
|
+
return rawValue;
|
|
50
|
+
}
|
|
51
|
+
function parseIntegerFlag(flagName, rawValue) {
|
|
52
|
+
const parsed = Number.parseInt(parseStringFlag(flagName, rawValue), 10);
|
|
53
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
54
|
+
throw new Error(`${flagName} must be a positive integer`);
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseCliArgs } from "./args.js";
|
|
7
|
+
import { writePlaceholderArtifacts } from "./output.js";
|
|
8
|
+
import { scanDataClasses } from "./resolver.js";
|
|
9
|
+
import { scanSpringProject } from "./scanner.js";
|
|
10
|
+
async function main() {
|
|
11
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
12
|
+
await validateProjectPath(options.projectPath);
|
|
13
|
+
const endpoints = await scanSpringProject(options.projectPath);
|
|
14
|
+
const types = await scanDataClasses(options.projectPath);
|
|
15
|
+
const output = await writePlaceholderArtifacts(options, endpoints, types);
|
|
16
|
+
printSummary(options, endpoints.length, output.warnings);
|
|
17
|
+
if (options.serve) {
|
|
18
|
+
await serveOutput(options.output, options.port);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function validateProjectPath(projectPath) {
|
|
22
|
+
const result = await stat(projectPath).catch(() => null);
|
|
23
|
+
if (!result || !result.isDirectory()) {
|
|
24
|
+
throw new Error(`Project path not found or not a directory: ${projectPath}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function printSummary(options, endpointCount, warnings) {
|
|
28
|
+
console.log("Spring API Scanner");
|
|
29
|
+
console.log(`- Project: ${path.resolve(options.projectPath)}`);
|
|
30
|
+
console.log(`- Output: ${path.resolve(options.output)}`);
|
|
31
|
+
console.log(`- Endpoints discovered: ${endpointCount}`);
|
|
32
|
+
console.log("- OpenAPI: openapi.json written");
|
|
33
|
+
console.log(`- Warnings: ${warnings.length}`);
|
|
34
|
+
for (const warning of warnings.slice(0, 5)) {
|
|
35
|
+
console.log(` - ${warning}`);
|
|
36
|
+
}
|
|
37
|
+
console.log(`- Serve mode: ${options.serve ? "enabled" : "disabled"}`);
|
|
38
|
+
}
|
|
39
|
+
async function serveOutput(outputDir, port) {
|
|
40
|
+
const root = path.resolve(outputDir);
|
|
41
|
+
const server = createServer(async (req, res) => {
|
|
42
|
+
const pathname = req.url === "/openapi.json"
|
|
43
|
+
? "openapi.json"
|
|
44
|
+
: req.url === "/ui-data.json"
|
|
45
|
+
? "ui-data.json"
|
|
46
|
+
: "index.html";
|
|
47
|
+
const fullPath = path.join(root, pathname);
|
|
48
|
+
try {
|
|
49
|
+
const contents = await readFile(fullPath);
|
|
50
|
+
const contentType = pathname === "index.html" ? "text/html; charset=utf-8" : "application/json; charset=utf-8";
|
|
51
|
+
res.writeHead(200, { "content-type": contentType });
|
|
52
|
+
res.end(contents);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
56
|
+
res.end("Not found");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
server.listen(port, () => {
|
|
60
|
+
console.log(`Serving docs at http://localhost:${port}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
main().catch((error) => {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
console.error(`Error: ${message}`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtractedEndpoint } from "./scanner.js";
|
|
2
|
+
import { type KotlinTypeRegistry } from "./resolver.js";
|
|
3
|
+
export interface OpenApiSchema {
|
|
4
|
+
type?: string;
|
|
5
|
+
format?: string;
|
|
6
|
+
items?: OpenApiSchema;
|
|
7
|
+
properties?: Record<string, OpenApiSchema>;
|
|
8
|
+
required?: string[];
|
|
9
|
+
$ref?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface OpenApiParameter {
|
|
12
|
+
in: "path" | "query" | "header";
|
|
13
|
+
name: string;
|
|
14
|
+
required: boolean;
|
|
15
|
+
schema: OpenApiSchema;
|
|
16
|
+
}
|
|
17
|
+
export interface OpenApiResponse {
|
|
18
|
+
description: string;
|
|
19
|
+
content: {
|
|
20
|
+
"application/json": {
|
|
21
|
+
schema: OpenApiSchema;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface OpenApiOperation {
|
|
26
|
+
operationId: string;
|
|
27
|
+
tags: string[];
|
|
28
|
+
parameters: OpenApiParameter[];
|
|
29
|
+
requestBody?: {
|
|
30
|
+
required: boolean;
|
|
31
|
+
content: {
|
|
32
|
+
"application/json": {
|
|
33
|
+
schema: OpenApiSchema;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
responses: Record<string, OpenApiResponse | {
|
|
38
|
+
description: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export interface OpenApiDocument {
|
|
42
|
+
openapi: string;
|
|
43
|
+
info: {
|
|
44
|
+
title: string;
|
|
45
|
+
version: string;
|
|
46
|
+
};
|
|
47
|
+
paths: Record<string, Record<string, OpenApiOperation>>;
|
|
48
|
+
components: {
|
|
49
|
+
schemas: Record<string, OpenApiSchema>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface OpenApiArtifacts {
|
|
53
|
+
document: OpenApiDocument;
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}
|
|
56
|
+
export declare function buildOpenApiDocument(input: {
|
|
57
|
+
title: string;
|
|
58
|
+
version: string;
|
|
59
|
+
endpoints: ExtractedEndpoint[];
|
|
60
|
+
types: KotlinTypeRegistry;
|
|
61
|
+
}): OpenApiDocument;
|
|
62
|
+
export declare function buildOpenApiArtifacts(input: {
|
|
63
|
+
title: string;
|
|
64
|
+
version: string;
|
|
65
|
+
endpoints: ExtractedEndpoint[];
|
|
66
|
+
types: KotlinTypeRegistry;
|
|
67
|
+
}): OpenApiArtifacts;
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resolveSchemaForType } from "./resolver.js";
|
|
2
|
+
export function buildOpenApiDocument(input) {
|
|
3
|
+
return buildOpenApiArtifacts(input).document;
|
|
4
|
+
}
|
|
5
|
+
export function buildOpenApiArtifacts(input) {
|
|
6
|
+
const schemas = {};
|
|
7
|
+
const paths = {};
|
|
8
|
+
const context = { warnings: new Set() };
|
|
9
|
+
for (const endpoint of input.endpoints) {
|
|
10
|
+
const method = endpoint.httpMethod.toLowerCase();
|
|
11
|
+
const parameters = [
|
|
12
|
+
...endpoint.pathVariables.map((item) => ({
|
|
13
|
+
in: "path",
|
|
14
|
+
name: item.name,
|
|
15
|
+
required: true,
|
|
16
|
+
schema: resolveSchemaForType(item.type, input.types, schemas, new Set(), context)
|
|
17
|
+
})),
|
|
18
|
+
...endpoint.queryParams.map((item) => ({
|
|
19
|
+
in: "query",
|
|
20
|
+
name: item.name,
|
|
21
|
+
required: item.required ?? false,
|
|
22
|
+
schema: resolveSchemaForType(item.type, input.types, schemas, new Set(), context)
|
|
23
|
+
})),
|
|
24
|
+
...endpoint.headers.map((item) => ({
|
|
25
|
+
in: "header",
|
|
26
|
+
name: item.name,
|
|
27
|
+
required: item.required ?? false,
|
|
28
|
+
schema: resolveSchemaForType(item.type, input.types, schemas, new Set(), context)
|
|
29
|
+
}))
|
|
30
|
+
];
|
|
31
|
+
const requestBody = endpoint.requestBody
|
|
32
|
+
? {
|
|
33
|
+
required: endpoint.requestBody.required,
|
|
34
|
+
content: {
|
|
35
|
+
"application/json": {
|
|
36
|
+
schema: resolveSchemaForType(endpoint.requestBody.type, input.types, schemas, new Set(), context)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
: undefined;
|
|
41
|
+
const response = buildOperationResponse(endpoint, input.types, schemas, context);
|
|
42
|
+
const operation = {
|
|
43
|
+
operationId: endpoint.operationName,
|
|
44
|
+
tags: [guessTag(endpoint.sourceFile)],
|
|
45
|
+
parameters,
|
|
46
|
+
...(requestBody ? { requestBody } : {}),
|
|
47
|
+
responses: response
|
|
48
|
+
};
|
|
49
|
+
if (!paths[endpoint.fullPath]) {
|
|
50
|
+
paths[endpoint.fullPath] = {};
|
|
51
|
+
}
|
|
52
|
+
paths[endpoint.fullPath][method] = operation;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
document: {
|
|
56
|
+
openapi: "3.0.3",
|
|
57
|
+
info: {
|
|
58
|
+
title: input.title,
|
|
59
|
+
version: input.version
|
|
60
|
+
},
|
|
61
|
+
paths,
|
|
62
|
+
components: {
|
|
63
|
+
schemas
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
warnings: [...context.warnings]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function guessTag(sourceFile) {
|
|
70
|
+
const file = sourceFile.replaceAll("\\", "/").split("/").at(-1) ?? sourceFile;
|
|
71
|
+
return file.endsWith(".kt") ? file.slice(0, -3) : file;
|
|
72
|
+
}
|
|
73
|
+
function buildOperationResponse(endpoint, types, schemas, context) {
|
|
74
|
+
const normalizedReturn = endpoint.returnType.trim();
|
|
75
|
+
const isNoContent = normalizedReturn === "Unit" || normalizedReturn === "Void";
|
|
76
|
+
if (isNoContent) {
|
|
77
|
+
return {
|
|
78
|
+
"204": {
|
|
79
|
+
description: "No Content"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const statusCode = endpoint.httpMethod === "POST" ? "201" : "200";
|
|
84
|
+
return {
|
|
85
|
+
[statusCode]: {
|
|
86
|
+
description: statusCode === "201" ? "Created" : "OK",
|
|
87
|
+
content: {
|
|
88
|
+
"application/json": {
|
|
89
|
+
schema: resolveSchemaForType(endpoint.returnType, types, schemas, new Set(), context)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CliOptions } from "./args.js";
|
|
2
|
+
import type { KotlinTypeRegistry } from "./resolver.js";
|
|
3
|
+
import type { ExtractedEndpoint } from "./scanner.js";
|
|
4
|
+
export declare function writePlaceholderArtifacts(options: CliOptions, endpoints: ExtractedEndpoint[], types: KotlinTypeRegistry): Promise<{
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}>;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildOpenApiArtifacts } from "./openapi.js";
|
|
4
|
+
import { buildUiModel, renderUiHtml } from "./ui.js";
|
|
5
|
+
export async function writePlaceholderArtifacts(options, endpoints, types) {
|
|
6
|
+
const outputDir = path.resolve(options.output);
|
|
7
|
+
const openApiPath = path.join(outputDir, "openapi.json");
|
|
8
|
+
const uiDataPath = path.join(outputDir, "ui-data.json");
|
|
9
|
+
const indexPath = path.join(outputDir, "index.html");
|
|
10
|
+
const artifacts = buildOpenApiArtifacts({
|
|
11
|
+
title: options.title,
|
|
12
|
+
version: options.version,
|
|
13
|
+
endpoints,
|
|
14
|
+
types
|
|
15
|
+
});
|
|
16
|
+
const uiModel = buildUiModel({
|
|
17
|
+
endpoints,
|
|
18
|
+
openapi: artifacts.document
|
|
19
|
+
});
|
|
20
|
+
await mkdir(outputDir, { recursive: true });
|
|
21
|
+
await writeFile(openApiPath, JSON.stringify(artifacts.document, null, 2), "utf8");
|
|
22
|
+
await writeFile(uiDataPath, JSON.stringify(uiModel, null, 2), "utf8");
|
|
23
|
+
await writeFile(indexPath, renderUiHtml(options.title, uiModel), "utf8");
|
|
24
|
+
return { warnings: artifacts.warnings };
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface KotlinProperty {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
nullable: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface KotlinDataClass {
|
|
7
|
+
name: string;
|
|
8
|
+
namingStrategy: "snake_case" | "camelCase";
|
|
9
|
+
properties: KotlinProperty[];
|
|
10
|
+
}
|
|
11
|
+
export type KotlinTypeRegistry = Record<string, KotlinDataClass>;
|
|
12
|
+
export interface ResolutionContext {
|
|
13
|
+
warnings: Set<string>;
|
|
14
|
+
}
|
|
15
|
+
export declare function scanDataClasses(projectPath: string): Promise<KotlinTypeRegistry>;
|
|
16
|
+
export declare function parseDataClassesFromSource(source: string): KotlinTypeRegistry;
|
|
17
|
+
export declare function resolveSchemaForType(rawType: string, registry: KotlinTypeRegistry, components: Record<string, unknown>, seen?: Set<string>, context?: ResolutionContext): Record<string, unknown>;
|