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/src/openapi.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ExtractedEndpoint } from "./scanner.js";
|
|
2
|
+
import { resolveSchemaForType, type KotlinTypeRegistry, type ResolutionContext } from "./resolver.js";
|
|
3
|
+
|
|
4
|
+
export interface OpenApiSchema {
|
|
5
|
+
type?: string;
|
|
6
|
+
format?: string;
|
|
7
|
+
items?: OpenApiSchema;
|
|
8
|
+
properties?: Record<string, OpenApiSchema>;
|
|
9
|
+
required?: string[];
|
|
10
|
+
$ref?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OpenApiParameter {
|
|
14
|
+
in: "path" | "query" | "header";
|
|
15
|
+
name: string;
|
|
16
|
+
required: boolean;
|
|
17
|
+
schema: OpenApiSchema;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OpenApiResponse {
|
|
21
|
+
description: string;
|
|
22
|
+
content: {
|
|
23
|
+
"application/json": {
|
|
24
|
+
schema: OpenApiSchema;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OpenApiOperation {
|
|
30
|
+
operationId: string;
|
|
31
|
+
tags: string[];
|
|
32
|
+
parameters: OpenApiParameter[];
|
|
33
|
+
requestBody?: {
|
|
34
|
+
required: boolean;
|
|
35
|
+
content: {
|
|
36
|
+
"application/json": {
|
|
37
|
+
schema: OpenApiSchema;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
responses: Record<string, OpenApiResponse | { description: string }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface OpenApiDocument {
|
|
45
|
+
openapi: string;
|
|
46
|
+
info: {
|
|
47
|
+
title: string;
|
|
48
|
+
version: string;
|
|
49
|
+
};
|
|
50
|
+
paths: Record<string, Record<string, OpenApiOperation>>;
|
|
51
|
+
components: {
|
|
52
|
+
schemas: Record<string, OpenApiSchema>;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OpenApiArtifacts {
|
|
57
|
+
document: OpenApiDocument;
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildOpenApiDocument(input: {
|
|
62
|
+
title: string;
|
|
63
|
+
version: string;
|
|
64
|
+
endpoints: ExtractedEndpoint[];
|
|
65
|
+
types: KotlinTypeRegistry;
|
|
66
|
+
}): OpenApiDocument {
|
|
67
|
+
return buildOpenApiArtifacts(input).document;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildOpenApiArtifacts(input: {
|
|
71
|
+
title: string;
|
|
72
|
+
version: string;
|
|
73
|
+
endpoints: ExtractedEndpoint[];
|
|
74
|
+
types: KotlinTypeRegistry;
|
|
75
|
+
}): OpenApiArtifacts {
|
|
76
|
+
const schemas: Record<string, OpenApiSchema> = {};
|
|
77
|
+
const paths: Record<string, Record<string, OpenApiOperation>> = {};
|
|
78
|
+
const context: ResolutionContext = { warnings: new Set() };
|
|
79
|
+
|
|
80
|
+
for (const endpoint of input.endpoints) {
|
|
81
|
+
const method = endpoint.httpMethod.toLowerCase();
|
|
82
|
+
const parameters: OpenApiParameter[] = [
|
|
83
|
+
...endpoint.pathVariables.map((item): OpenApiParameter => ({
|
|
84
|
+
in: "path",
|
|
85
|
+
name: item.name,
|
|
86
|
+
required: true,
|
|
87
|
+
schema: resolveSchemaForType(item.type, input.types, schemas, new Set(), context) as OpenApiSchema
|
|
88
|
+
})),
|
|
89
|
+
...endpoint.queryParams.map((item): OpenApiParameter => ({
|
|
90
|
+
in: "query",
|
|
91
|
+
name: item.name,
|
|
92
|
+
required: item.required ?? false,
|
|
93
|
+
schema: resolveSchemaForType(item.type, input.types, schemas, new Set(), context) as OpenApiSchema
|
|
94
|
+
})),
|
|
95
|
+
...endpoint.headers.map((item): OpenApiParameter => ({
|
|
96
|
+
in: "header",
|
|
97
|
+
name: item.name,
|
|
98
|
+
required: item.required ?? false,
|
|
99
|
+
schema: resolveSchemaForType(item.type, input.types, schemas, new Set(), context) as OpenApiSchema
|
|
100
|
+
}))
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const requestBody = endpoint.requestBody
|
|
104
|
+
? {
|
|
105
|
+
required: endpoint.requestBody.required,
|
|
106
|
+
content: {
|
|
107
|
+
"application/json": {
|
|
108
|
+
schema: resolveSchemaForType(
|
|
109
|
+
endpoint.requestBody.type,
|
|
110
|
+
input.types,
|
|
111
|
+
schemas,
|
|
112
|
+
new Set(),
|
|
113
|
+
context
|
|
114
|
+
) as OpenApiSchema
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
const response = buildOperationResponse(endpoint, input.types, schemas, context);
|
|
121
|
+
|
|
122
|
+
const operation: OpenApiOperation = {
|
|
123
|
+
operationId: endpoint.operationName,
|
|
124
|
+
tags: [guessTag(endpoint.sourceFile)],
|
|
125
|
+
parameters,
|
|
126
|
+
...(requestBody ? { requestBody } : {}),
|
|
127
|
+
responses: response
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (!paths[endpoint.fullPath]) {
|
|
131
|
+
paths[endpoint.fullPath] = {};
|
|
132
|
+
}
|
|
133
|
+
paths[endpoint.fullPath][method] = operation;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
document: {
|
|
138
|
+
openapi: "3.0.3",
|
|
139
|
+
info: {
|
|
140
|
+
title: input.title,
|
|
141
|
+
version: input.version
|
|
142
|
+
},
|
|
143
|
+
paths,
|
|
144
|
+
components: {
|
|
145
|
+
schemas
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
warnings: [...context.warnings]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function guessTag(sourceFile: string): string {
|
|
153
|
+
const file = sourceFile.replaceAll("\\", "/").split("/").at(-1) ?? sourceFile;
|
|
154
|
+
return file.endsWith(".kt") ? file.slice(0, -3) : file;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildOperationResponse(
|
|
158
|
+
endpoint: ExtractedEndpoint,
|
|
159
|
+
types: KotlinTypeRegistry,
|
|
160
|
+
schemas: Record<string, OpenApiSchema>,
|
|
161
|
+
context: ResolutionContext
|
|
162
|
+
): Record<string, OpenApiResponse | { description: string }> {
|
|
163
|
+
const normalizedReturn = endpoint.returnType.trim();
|
|
164
|
+
const isNoContent = normalizedReturn === "Unit" || normalizedReturn === "Void";
|
|
165
|
+
|
|
166
|
+
if (isNoContent) {
|
|
167
|
+
return {
|
|
168
|
+
"204": {
|
|
169
|
+
description: "No Content"
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const statusCode = endpoint.httpMethod === "POST" ? "201" : "200";
|
|
175
|
+
return {
|
|
176
|
+
[statusCode]: {
|
|
177
|
+
description: statusCode === "201" ? "Created" : "OK",
|
|
178
|
+
content: {
|
|
179
|
+
"application/json": {
|
|
180
|
+
schema: resolveSchemaForType(
|
|
181
|
+
endpoint.returnType,
|
|
182
|
+
types,
|
|
183
|
+
schemas,
|
|
184
|
+
new Set(),
|
|
185
|
+
context
|
|
186
|
+
) as OpenApiSchema
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
package/src/output.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { CliOptions } from "./args.js";
|
|
4
|
+
import { buildOpenApiArtifacts } from "./openapi.js";
|
|
5
|
+
import type { KotlinTypeRegistry } from "./resolver.js";
|
|
6
|
+
import type { ExtractedEndpoint } from "./scanner.js";
|
|
7
|
+
import { buildUiModel, renderUiHtml } from "./ui.js";
|
|
8
|
+
|
|
9
|
+
export async function writePlaceholderArtifacts(
|
|
10
|
+
options: CliOptions,
|
|
11
|
+
endpoints: ExtractedEndpoint[],
|
|
12
|
+
types: KotlinTypeRegistry
|
|
13
|
+
): Promise<{ warnings: string[] }> {
|
|
14
|
+
const outputDir = path.resolve(options.output);
|
|
15
|
+
const openApiPath = path.join(outputDir, "openapi.json");
|
|
16
|
+
const uiDataPath = path.join(outputDir, "ui-data.json");
|
|
17
|
+
const indexPath = path.join(outputDir, "index.html");
|
|
18
|
+
const artifacts = buildOpenApiArtifacts({
|
|
19
|
+
title: options.title,
|
|
20
|
+
version: options.version,
|
|
21
|
+
endpoints,
|
|
22
|
+
types
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const uiModel = buildUiModel({
|
|
26
|
+
endpoints,
|
|
27
|
+
openapi: artifacts.document
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await mkdir(outputDir, { recursive: true });
|
|
31
|
+
await writeFile(openApiPath, JSON.stringify(artifacts.document, null, 2), "utf8");
|
|
32
|
+
await writeFile(uiDataPath, JSON.stringify(uiModel, null, 2), "utf8");
|
|
33
|
+
await writeFile(indexPath, renderUiHtml(options.title, uiModel), "utf8");
|
|
34
|
+
|
|
35
|
+
return { warnings: artifacts.warnings };
|
|
36
|
+
}
|
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface KotlinProperty {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
nullable: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface KotlinDataClass {
|
|
11
|
+
name: string;
|
|
12
|
+
namingStrategy: "snake_case" | "camelCase";
|
|
13
|
+
properties: KotlinProperty[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type KotlinTypeRegistry = Record<string, KotlinDataClass>;
|
|
17
|
+
|
|
18
|
+
export interface ResolutionContext {
|
|
19
|
+
warnings: Set<string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function scanDataClasses(projectPath: string): Promise<KotlinTypeRegistry> {
|
|
23
|
+
const kotlinRoot = path.join(projectPath, "src", "main", "kotlin");
|
|
24
|
+
const files = (await listKotlinFiles(kotlinRoot)).sort();
|
|
25
|
+
const registry: KotlinTypeRegistry = {};
|
|
26
|
+
|
|
27
|
+
for (const filePath of files) {
|
|
28
|
+
const source = await readFile(filePath, "utf8");
|
|
29
|
+
Object.assign(registry, parseDataClassesFromSource(source));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return registry;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseDataClassesFromSource(source: string): KotlinTypeRegistry {
|
|
36
|
+
const registry: KotlinTypeRegistry = {};
|
|
37
|
+
const matches = source.matchAll(
|
|
38
|
+
/((?:\s*@[^\n]+\n)*)\s*data\s+class\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*?)\)/g
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
for (const match of matches) {
|
|
42
|
+
const annotationBlock = match[1] ?? "";
|
|
43
|
+
const name = match[2] ?? "";
|
|
44
|
+
const rawProperties = match[3] ?? "";
|
|
45
|
+
const properties = splitTopLevel(rawProperties)
|
|
46
|
+
.map((value) => value.trim())
|
|
47
|
+
.filter((value) => value.length > 0)
|
|
48
|
+
.map(parseProperty)
|
|
49
|
+
.filter((value): value is KotlinProperty => value !== null);
|
|
50
|
+
|
|
51
|
+
registry[name] = {
|
|
52
|
+
name,
|
|
53
|
+
namingStrategy: parseNamingStrategy(annotationBlock),
|
|
54
|
+
properties
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return registry;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveSchemaForType(
|
|
62
|
+
rawType: string,
|
|
63
|
+
registry: KotlinTypeRegistry,
|
|
64
|
+
components: Record<string, unknown>,
|
|
65
|
+
seen: Set<string> = new Set(),
|
|
66
|
+
context?: ResolutionContext
|
|
67
|
+
): Record<string, unknown> {
|
|
68
|
+
const typeName = cleanType(rawType);
|
|
69
|
+
const unwrapped = normalizeNullableType(unwrapEntity(typeName));
|
|
70
|
+
|
|
71
|
+
if (isListLike(unwrapped)) {
|
|
72
|
+
const inner = extractSingleGeneric(unwrapped);
|
|
73
|
+
return {
|
|
74
|
+
type: "array",
|
|
75
|
+
items: resolveSchemaForType(inner, registry, components, seen, context)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const primitive = mapPrimitive(unwrapped);
|
|
80
|
+
if (primitive) {
|
|
81
|
+
return primitive;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (registry[unwrapped]) {
|
|
85
|
+
ensureComponent(unwrapped, registry, components, seen, context);
|
|
86
|
+
return { $ref: `#/components/schemas/${unwrapped}` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (context) {
|
|
90
|
+
context.warnings.add(`Unresolved type: ${unwrapped}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { type: "object" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureComponent(
|
|
97
|
+
typeName: string,
|
|
98
|
+
registry: KotlinTypeRegistry,
|
|
99
|
+
components: Record<string, unknown>,
|
|
100
|
+
seen: Set<string>,
|
|
101
|
+
context?: ResolutionContext
|
|
102
|
+
): void {
|
|
103
|
+
if (components[typeName] || seen.has(typeName)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
seen.add(typeName);
|
|
107
|
+
|
|
108
|
+
const dto = registry[typeName];
|
|
109
|
+
if (!dto) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const properties: Record<string, unknown> = {};
|
|
114
|
+
const required: string[] = [];
|
|
115
|
+
for (const property of dto.properties) {
|
|
116
|
+
const clean = cleanType(property.type);
|
|
117
|
+
const serializedName = serializePropertyName(dto.namingStrategy, property.name);
|
|
118
|
+
properties[serializedName] = resolveSchemaForType(clean, registry, components, seen, context);
|
|
119
|
+
if (!property.nullable) {
|
|
120
|
+
required.push(serializedName);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
components[typeName] = {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties,
|
|
127
|
+
required
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseProperty(raw: string): KotlinProperty | null {
|
|
132
|
+
const parsed = raw.match(/(?:val|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=]+?)(?:\s*=.*)?$/);
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const name = parsed[1] ?? "";
|
|
138
|
+
const type = cleanType(parsed[2] ?? "Any");
|
|
139
|
+
return { name, type, nullable: type.endsWith("?") };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cleanType(rawType: string): string {
|
|
143
|
+
return rawType.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeNullableType(typeName: string): string {
|
|
147
|
+
return typeName.endsWith("?") ? typeName.slice(0, -1) : typeName;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function unwrapEntity(typeName: string): string {
|
|
151
|
+
const outer = outerType(typeName);
|
|
152
|
+
if (outer === "ResponseEntity") {
|
|
153
|
+
return unwrapEntity(extractSingleGeneric(typeName));
|
|
154
|
+
}
|
|
155
|
+
return typeName;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isListLike(typeName: string): boolean {
|
|
159
|
+
const outer = outerType(typeName);
|
|
160
|
+
return outer === "List" || outer === "MutableList" || outer === "Page";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function outerType(typeName: string): string {
|
|
164
|
+
const idx = typeName.indexOf("<");
|
|
165
|
+
return idx < 0 ? typeName.replace("?", "") : typeName.slice(0, idx).trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractSingleGeneric(typeName: string): string {
|
|
169
|
+
const start = typeName.indexOf("<");
|
|
170
|
+
const end = typeName.lastIndexOf(">");
|
|
171
|
+
if (start < 0 || end < 0 || end <= start + 1) {
|
|
172
|
+
return "Any";
|
|
173
|
+
}
|
|
174
|
+
return typeName.slice(start + 1, end).trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mapPrimitive(typeName: string): Record<string, unknown> | null {
|
|
178
|
+
const withoutNullable = typeName.endsWith("?") ? typeName.slice(0, -1) : typeName;
|
|
179
|
+
switch (withoutNullable) {
|
|
180
|
+
case "String":
|
|
181
|
+
return { type: "string" };
|
|
182
|
+
case "Long":
|
|
183
|
+
return { type: "integer", format: "int64" };
|
|
184
|
+
case "Int":
|
|
185
|
+
return { type: "integer", format: "int32" };
|
|
186
|
+
case "Boolean":
|
|
187
|
+
return { type: "boolean" };
|
|
188
|
+
case "Double":
|
|
189
|
+
return { type: "number", format: "double" };
|
|
190
|
+
case "Float":
|
|
191
|
+
return { type: "number", format: "float" };
|
|
192
|
+
case "Instant":
|
|
193
|
+
return { type: "string", format: "date-time" };
|
|
194
|
+
default:
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function listKotlinFiles(root: string): Promise<string[]> {
|
|
200
|
+
const entries = (await readdir(root, { withFileTypes: true }).catch(() => [])).sort((a, b) =>
|
|
201
|
+
a.name.localeCompare(b.name)
|
|
202
|
+
);
|
|
203
|
+
const files: string[] = [];
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const fullPath = path.join(root, entry.name);
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
files.push(...(await listKotlinFiles(fullPath)));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (entry.isFile() && fullPath.endsWith(".kt")) {
|
|
212
|
+
files.push(fullPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return files;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function splitTopLevel(input: string): string[] {
|
|
220
|
+
const parts: string[] = [];
|
|
221
|
+
let current = "";
|
|
222
|
+
let depthParen = 0;
|
|
223
|
+
let depthAngle = 0;
|
|
224
|
+
let depthSquare = 0;
|
|
225
|
+
|
|
226
|
+
for (const char of input) {
|
|
227
|
+
if (char === "(") depthParen += 1;
|
|
228
|
+
if (char === ")") depthParen -= 1;
|
|
229
|
+
if (char === "<") depthAngle += 1;
|
|
230
|
+
if (char === ">") depthAngle -= 1;
|
|
231
|
+
if (char === "[") depthSquare += 1;
|
|
232
|
+
if (char === "]") depthSquare -= 1;
|
|
233
|
+
|
|
234
|
+
if (char === "," && depthParen === 0 && depthAngle === 0 && depthSquare === 0) {
|
|
235
|
+
parts.push(current);
|
|
236
|
+
current = "";
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
current += char;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (current.trim().length > 0) {
|
|
244
|
+
parts.push(current);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return parts;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function parseNamingStrategy(annotationBlock: string): "snake_case" | "camelCase" {
|
|
251
|
+
if (!/@JsonNaming\s*\(/.test(annotationBlock)) {
|
|
252
|
+
return "snake_case";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (/LowerCamelCaseStrategy/.test(annotationBlock)) {
|
|
256
|
+
return "camelCase";
|
|
257
|
+
}
|
|
258
|
+
if (/SnakeCaseStrategy/.test(annotationBlock)) {
|
|
259
|
+
return "snake_case";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return "snake_case";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function serializePropertyName(strategy: "snake_case" | "camelCase", name: string): string {
|
|
266
|
+
if (strategy === "camelCase") {
|
|
267
|
+
return name;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return name
|
|
271
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
272
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
273
|
+
.toLowerCase();
|
|
274
|
+
}
|