svelte-reflector 1.3.2 → 1.3.4
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/dist/core/api/ApiClassBuilder.d.ts +23 -0
- package/dist/core/api/ApiClassBuilder.js +125 -0
- package/dist/core/api/ApiFileBuilder.d.ts +12 -0
- package/dist/core/api/ApiFileBuilder.js +24 -0
- package/dist/core/api/ApiMethodGenerator.d.ts +17 -0
- package/dist/core/api/ApiMethodGenerator.js +176 -0
- package/dist/core/api/ApiParamProcessor.d.ts +24 -0
- package/dist/core/api/ApiParamProcessor.js +36 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +5 -0
- package/dist/core/method/Method.d.ts +1 -0
- package/dist/core/method/MethodBuilder.js +8 -3
- package/dist/core/method/MethodResponseAnalyzer.d.ts +2 -0
- package/dist/core/method/MethodResponseAnalyzer.js +21 -6
- package/dist/core/method/generators/MethodApiCallBuilder.js +2 -1
- package/dist/core/method/generators/MethodGenerator.js +4 -1
- package/dist/core/module/ModuleClassBuilder.d.ts +1 -0
- package/dist/core/module/ModuleClassBuilder.js +5 -4
- package/dist/core/module/ModuleImports.d.ts +1 -0
- package/dist/core/module/ModuleImports.js +6 -1
- package/dist/core/module/ModuleMethodProcessor.js +7 -7
- package/dist/generate-doc.js +5 -1
- package/dist/main.d.ts +31 -0
- package/dist/main.js +148 -7
- package/dist/method.d.ts +3 -0
- package/dist/method.js +6 -0
- package/dist/module.d.ts +4 -0
- package/dist/module.js +36 -3
- package/dist/props/primitive.property.js +2 -1
- package/dist/reflector.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Method } from "../../method.js";
|
|
2
|
+
import type { ModuleImports } from "../module/ModuleImports.js";
|
|
3
|
+
import type { ModuleClassBuilder } from "../module/ModuleClassBuilder.js";
|
|
4
|
+
export interface ApiEndpointBlock {
|
|
5
|
+
paramCode: string;
|
|
6
|
+
classCode: string;
|
|
7
|
+
schemaEntries: Set<string>;
|
|
8
|
+
}
|
|
9
|
+
export declare class ApiClassBuilder {
|
|
10
|
+
private readonly imports;
|
|
11
|
+
private readonly methodGenerator;
|
|
12
|
+
private readonly paramProcessor;
|
|
13
|
+
constructor(params: {
|
|
14
|
+
imports: ModuleImports;
|
|
15
|
+
classBuilder: ModuleClassBuilder;
|
|
16
|
+
});
|
|
17
|
+
build(params: {
|
|
18
|
+
method: Method;
|
|
19
|
+
}): ApiEndpointBlock | null;
|
|
20
|
+
private buildStateProperties;
|
|
21
|
+
private buildResetLines;
|
|
22
|
+
private shouldSkipMethod;
|
|
23
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { capitalizeFirstLetter, createDangerMessage, treatByUppercase } from "../../helpers/helpers.js";
|
|
2
|
+
import { ApiMethodGenerator } from "./ApiMethodGenerator.js";
|
|
3
|
+
import { ApiParamProcessor } from "./ApiParamProcessor.js";
|
|
4
|
+
export class ApiClassBuilder {
|
|
5
|
+
imports;
|
|
6
|
+
methodGenerator = new ApiMethodGenerator();
|
|
7
|
+
paramProcessor;
|
|
8
|
+
constructor(params) {
|
|
9
|
+
this.imports = params.imports;
|
|
10
|
+
this.paramProcessor = new ApiParamProcessor({
|
|
11
|
+
imports: params.imports,
|
|
12
|
+
classBuilder: params.classBuilder,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
build(params) {
|
|
16
|
+
const { method } = params;
|
|
17
|
+
if (this.shouldSkipMethod(method))
|
|
18
|
+
return null;
|
|
19
|
+
const { request, headers, cookies, paths, querys } = method;
|
|
20
|
+
const { bodyType, responseType, attributeType, isPrimitiveResponse } = request;
|
|
21
|
+
// Process per-endpoint params
|
|
22
|
+
const processedParams = this.paramProcessor.process({
|
|
23
|
+
methodName: capitalizeFirstLetter(method.name),
|
|
24
|
+
querys,
|
|
25
|
+
paths,
|
|
26
|
+
headers,
|
|
27
|
+
cookies,
|
|
28
|
+
});
|
|
29
|
+
// Build state properties
|
|
30
|
+
const stateProps = this.buildStateProperties(method);
|
|
31
|
+
// Build the call method
|
|
32
|
+
const callMethod = this.methodGenerator.generate(method);
|
|
33
|
+
// Build reset method
|
|
34
|
+
const resetLines = this.buildResetLines(method, processedParams.paramReset);
|
|
35
|
+
// Collect schema entries
|
|
36
|
+
const schemaEntries = new Set();
|
|
37
|
+
if (bodyType) {
|
|
38
|
+
schemaEntries.add(bodyType);
|
|
39
|
+
}
|
|
40
|
+
if (responseType && responseType !== "response" && !isPrimitiveResponse) {
|
|
41
|
+
schemaEntries.add(`type ${responseType}Interface`);
|
|
42
|
+
schemaEntries.add(responseType);
|
|
43
|
+
}
|
|
44
|
+
// Handle form imports
|
|
45
|
+
if (attributeType === "form" && bodyType) {
|
|
46
|
+
this.imports.addReflectorImport("isFormValid");
|
|
47
|
+
}
|
|
48
|
+
// Handle list imports
|
|
49
|
+
if (attributeType === "list") {
|
|
50
|
+
this.imports.addReflectorImport("genericArrayBundler");
|
|
51
|
+
}
|
|
52
|
+
const className = capitalizeFirstLetter(method.name);
|
|
53
|
+
const classCode = `
|
|
54
|
+
export class ${className} {
|
|
55
|
+
${stateProps.join(";")}
|
|
56
|
+
${processedParams.paramAttributes.map((a) => `${a};`).join("\n ")}
|
|
57
|
+
|
|
58
|
+
${callMethod}
|
|
59
|
+
|
|
60
|
+
reset() {
|
|
61
|
+
${resetLines.join(";")}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
const paramCode = processedParams.paramClasses.join("\n");
|
|
66
|
+
return { paramCode, classCode, schemaEntries };
|
|
67
|
+
}
|
|
68
|
+
buildStateProperties(method) {
|
|
69
|
+
const { attributeType, responseType, bodyType, isPrimitiveResponse } = method.request;
|
|
70
|
+
const props = ["loading = $state<boolean>(false)"];
|
|
71
|
+
if (attributeType === "form" && bodyType) {
|
|
72
|
+
// For form endpoints, the form IS the data
|
|
73
|
+
props.push(`form = new ${bodyType}()`);
|
|
74
|
+
}
|
|
75
|
+
else if (attributeType === "list") {
|
|
76
|
+
props.push(`data = $state<${responseType}['data']>([])`);
|
|
77
|
+
props.push("totalPages = $state<number>(1)");
|
|
78
|
+
}
|
|
79
|
+
else if (attributeType === "entity" && responseType && !isPrimitiveResponse) {
|
|
80
|
+
props.push(`data = $state<${responseType} | undefined>()`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
props.push("data = $state<unknown>(undefined)");
|
|
84
|
+
}
|
|
85
|
+
return props;
|
|
86
|
+
}
|
|
87
|
+
buildResetLines(method, paramReset) {
|
|
88
|
+
const { attributeType, bodyType } = method.request;
|
|
89
|
+
const lines = [];
|
|
90
|
+
if (attributeType === "form" && bodyType) {
|
|
91
|
+
lines.push(`this.form = new ${bodyType}()`);
|
|
92
|
+
}
|
|
93
|
+
else if (attributeType === "list") {
|
|
94
|
+
lines.push("this.data = []");
|
|
95
|
+
lines.push("this.totalPages = 1");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
lines.push("this.data = undefined");
|
|
99
|
+
}
|
|
100
|
+
lines.push(...paramReset);
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
shouldSkipMethod(method) {
|
|
104
|
+
const { bodyType, responseType, attributeType } = method.request;
|
|
105
|
+
if (bodyType === "string") {
|
|
106
|
+
createDangerMessage(`Method ${method.name} was skipped because it has an invalid body.`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const isNullResponse = !responseType || responseType === "null";
|
|
110
|
+
if ((attributeType === "entity" || attributeType === "list") && isNullResponse) {
|
|
111
|
+
createDangerMessage(`Method ${method.name} was skipped because it has a null response.`);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const endpointParams = [...method.endpoint.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).filter(Boolean);
|
|
115
|
+
if (endpointParams.length > 0) {
|
|
116
|
+
const declaredPaths = new Set(method.paths.map((p) => p.name));
|
|
117
|
+
const undeclared = endpointParams.filter((p) => !declaredPaths.has(p));
|
|
118
|
+
if (undeclared.length > 0) {
|
|
119
|
+
createDangerMessage(`Method ${method.name} was skipped because it has undeclared path params: ${undeclared.join(", ")}`);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleImports } from "../module/ModuleImports.js";
|
|
2
|
+
import type { ApiEndpointBlock } from "./ApiClassBuilder.js";
|
|
3
|
+
export declare class ApiFileBuilder {
|
|
4
|
+
private readonly imports;
|
|
5
|
+
constructor(params: {
|
|
6
|
+
imports: ModuleImports;
|
|
7
|
+
});
|
|
8
|
+
build(params: {
|
|
9
|
+
endpointBlocks: ApiEndpointBlock[];
|
|
10
|
+
classImports: string;
|
|
11
|
+
}): string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class ApiFileBuilder {
|
|
2
|
+
imports;
|
|
3
|
+
constructor(params) {
|
|
4
|
+
this.imports = params.imports;
|
|
5
|
+
}
|
|
6
|
+
build(params) {
|
|
7
|
+
const { endpointBlocks, classImports } = params;
|
|
8
|
+
const reflectorImports = this.imports.buildReflectorImportsLine();
|
|
9
|
+
const enumImports = this.imports.buildEnumImportsLine();
|
|
10
|
+
const mockedImports = this.imports.buildMockedImportsLine();
|
|
11
|
+
const sections = endpointBlocks.map((block) => {
|
|
12
|
+
return `${block.paramCode}\n${block.classCode}`;
|
|
13
|
+
});
|
|
14
|
+
return `
|
|
15
|
+
${this.imports.getImportsArray().join(";")}
|
|
16
|
+
${reflectorImports}
|
|
17
|
+
${mockedImports}
|
|
18
|
+
${enumImports}
|
|
19
|
+
${classImports}
|
|
20
|
+
|
|
21
|
+
${sections.join("\n\n")}
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Method } from "../../method.js";
|
|
2
|
+
export declare class ApiMethodGenerator {
|
|
3
|
+
private readonly endpointBuilder;
|
|
4
|
+
generate(method: Method): string;
|
|
5
|
+
private buildProps;
|
|
6
|
+
private buildPathsInfo;
|
|
7
|
+
private buildApiCall;
|
|
8
|
+
private buildListCall;
|
|
9
|
+
private buildEntityCall;
|
|
10
|
+
private buildFormCall;
|
|
11
|
+
private buildDeleteCall;
|
|
12
|
+
private buildParamsType;
|
|
13
|
+
private buildDescription;
|
|
14
|
+
private buildMethodReturn;
|
|
15
|
+
private joinProps;
|
|
16
|
+
private buildQuerys;
|
|
17
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { MethodEndpointBuilder } from "../method/MethodEndpointBuilder.js";
|
|
2
|
+
export class ApiMethodGenerator {
|
|
3
|
+
endpointBuilder = new MethodEndpointBuilder();
|
|
4
|
+
generate(method) {
|
|
5
|
+
const description = this.buildDescription(method);
|
|
6
|
+
const endpoint = this.endpointBuilder.build(method.endpoint, method.paths);
|
|
7
|
+
const apiCall = this.buildApiCall(method);
|
|
8
|
+
const props = this.buildProps(method);
|
|
9
|
+
const pathsInfo = this.buildPathsInfo(method);
|
|
10
|
+
const paramsType = this.buildParamsType(method, pathsInfo);
|
|
11
|
+
return `
|
|
12
|
+
${description}
|
|
13
|
+
async call(params?: ${paramsType}) {
|
|
14
|
+
|
|
15
|
+
const behavior = params?.behavior ?? new Behavior();
|
|
16
|
+
const { onError, onSuccess } = behavior;
|
|
17
|
+
|
|
18
|
+
this.loading = true;
|
|
19
|
+
${props}
|
|
20
|
+
const endpoint = ${endpoint}
|
|
21
|
+
|
|
22
|
+
${apiCall.outside}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
${apiCall.inside}
|
|
26
|
+
await onSuccess?.(response);
|
|
27
|
+
|
|
28
|
+
return ${this.buildMethodReturn(method)};
|
|
29
|
+
} catch (e) {
|
|
30
|
+
let parsedError: ApiErrorResponse;
|
|
31
|
+
try {
|
|
32
|
+
parsedError = JSON.parse((e as Error).message) as ApiErrorResponse;
|
|
33
|
+
} catch {
|
|
34
|
+
parsedError = { error: 'unknown', message: (e as Error).message ?? String(e) };
|
|
35
|
+
}
|
|
36
|
+
return await onError?.(parsedError);
|
|
37
|
+
} finally {
|
|
38
|
+
this.loading = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
buildProps(method) {
|
|
44
|
+
const { querys, paths, cookies } = method;
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (querys.length > 0) {
|
|
47
|
+
lines.push(`const { ${querys.map((x) => x.name).join(", ")} } = this.querys.bundle()`);
|
|
48
|
+
}
|
|
49
|
+
if (paths.length > 0) {
|
|
50
|
+
lines.push(`const { ${paths.map((x) => x.name).join(", ")} } = params?.paths ?? this.paths`);
|
|
51
|
+
}
|
|
52
|
+
if (cookies.length > 0) {
|
|
53
|
+
lines.push(`const cookies = this.cookies`);
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
buildPathsInfo(method) {
|
|
58
|
+
const { paths } = method;
|
|
59
|
+
if (paths.length === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
return `{ ${paths
|
|
62
|
+
.map((p) => {
|
|
63
|
+
const type = p.rawType ?? p.type;
|
|
64
|
+
return `${p.name}: ${type}`;
|
|
65
|
+
})
|
|
66
|
+
.join("; ")} }`;
|
|
67
|
+
}
|
|
68
|
+
buildApiCall(method) {
|
|
69
|
+
const { attributeType, apiType, responseType, bodyType, hasEnumResponse } = method.request;
|
|
70
|
+
const responseTypeStr = hasEnumResponse && responseType ? responseType : method.responseTypeInterface;
|
|
71
|
+
if (attributeType === "list") {
|
|
72
|
+
return this.buildListCall(method, responseTypeStr);
|
|
73
|
+
}
|
|
74
|
+
if (attributeType === "entity") {
|
|
75
|
+
return this.buildEntityCall(method, responseTypeStr);
|
|
76
|
+
}
|
|
77
|
+
if (apiType === "post" || apiType === "put" || apiType === "patch") {
|
|
78
|
+
return this.buildFormCall(method, responseTypeStr, bodyType);
|
|
79
|
+
}
|
|
80
|
+
if (apiType === "delete") {
|
|
81
|
+
return this.buildDeleteCall(responseTypeStr);
|
|
82
|
+
}
|
|
83
|
+
return { inside: "", outside: "" };
|
|
84
|
+
}
|
|
85
|
+
buildListCall(method, responseType) {
|
|
86
|
+
const querys = this.joinProps(method.querys);
|
|
87
|
+
const inside = `
|
|
88
|
+
const response = await api.get<${responseType}, unknown>({
|
|
89
|
+
endpoint,
|
|
90
|
+
queryData: { ${querys} }
|
|
91
|
+
})
|
|
92
|
+
this.data = ${method.request.responseType}.from(response.data);
|
|
93
|
+
this.totalPages = response.totalPages;
|
|
94
|
+
`;
|
|
95
|
+
return { inside, outside: "" };
|
|
96
|
+
}
|
|
97
|
+
buildEntityCall(method, responseType) {
|
|
98
|
+
const rType = method.request.responseType;
|
|
99
|
+
const isPrimitive = method.request.isPrimitiveResponse;
|
|
100
|
+
const querys = this.buildQuerys(method.querys);
|
|
101
|
+
const buildedThisResponseType = rType && !isPrimitive
|
|
102
|
+
? `this.data = new ${rType}({ data: response })`
|
|
103
|
+
: "";
|
|
104
|
+
const inside = `
|
|
105
|
+
const response = await api.get<${responseType}, unknown>({
|
|
106
|
+
endpoint,
|
|
107
|
+
${querys}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
${buildedThisResponseType}
|
|
111
|
+
`;
|
|
112
|
+
return { inside, outside: "" };
|
|
113
|
+
}
|
|
114
|
+
buildFormCall(method, responseType, bodyType) {
|
|
115
|
+
const { apiType } = method.request;
|
|
116
|
+
const hasHeaders = method.headers.length > 0;
|
|
117
|
+
const hasData = !!bodyType;
|
|
118
|
+
const outside = [];
|
|
119
|
+
if (hasData) {
|
|
120
|
+
outside.push(`const data = this.form.bundle()`);
|
|
121
|
+
}
|
|
122
|
+
if (hasHeaders) {
|
|
123
|
+
outside.push(`const headers = this.headers.bundle()`);
|
|
124
|
+
}
|
|
125
|
+
const inside = `
|
|
126
|
+
const response = await api.${apiType}<${responseType}>({
|
|
127
|
+
endpoint,
|
|
128
|
+
${hasData ? "data," : ""}
|
|
129
|
+
${hasHeaders ? "headers," : ""}
|
|
130
|
+
})
|
|
131
|
+
`;
|
|
132
|
+
return { inside, outside: outside.join("\n") };
|
|
133
|
+
}
|
|
134
|
+
buildDeleteCall(responseType) {
|
|
135
|
+
const inside = `
|
|
136
|
+
const response = await api.delete<${responseType}, unknown>({
|
|
137
|
+
endpoint,
|
|
138
|
+
})
|
|
139
|
+
`;
|
|
140
|
+
return { inside, outside: "" };
|
|
141
|
+
}
|
|
142
|
+
buildParamsType(method, paramsPaths) {
|
|
143
|
+
const responseType = method.responseTypeInterface;
|
|
144
|
+
if (paramsPaths) {
|
|
145
|
+
return `ApiCallParams<${responseType}, ${paramsPaths}>`;
|
|
146
|
+
}
|
|
147
|
+
return `ApiCallParams<${responseType}>`;
|
|
148
|
+
}
|
|
149
|
+
buildDescription(method) {
|
|
150
|
+
return `/** ${method.description ?? ""} */`;
|
|
151
|
+
}
|
|
152
|
+
buildMethodReturn(method) {
|
|
153
|
+
const { attributeType, responseType, hasEnumResponse, isPrimitiveResponse } = method.request;
|
|
154
|
+
if (attributeType === "list") {
|
|
155
|
+
return "this.data";
|
|
156
|
+
}
|
|
157
|
+
if (!responseType) {
|
|
158
|
+
return "null";
|
|
159
|
+
}
|
|
160
|
+
if (hasEnumResponse) {
|
|
161
|
+
return "response.data";
|
|
162
|
+
}
|
|
163
|
+
if (isPrimitiveResponse) {
|
|
164
|
+
return "response";
|
|
165
|
+
}
|
|
166
|
+
return `new ${responseType}({ data: response })`;
|
|
167
|
+
}
|
|
168
|
+
joinProps(props) {
|
|
169
|
+
return props.map((x) => x.name).join(",");
|
|
170
|
+
}
|
|
171
|
+
buildQuerys(querys) {
|
|
172
|
+
if (querys.length === 0)
|
|
173
|
+
return "";
|
|
174
|
+
return `queryData: {${querys.map((q) => q.name).join(",")}}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AttributeProp } from "../../types/types.js";
|
|
2
|
+
import type { PrimitiveProp } from "../../props/primitive.property.js";
|
|
3
|
+
import type { ModuleImports } from "../module/ModuleImports.js";
|
|
4
|
+
import type { ModuleClassBuilder } from "../module/ModuleClassBuilder.js";
|
|
5
|
+
export interface ApiProcessedParams {
|
|
6
|
+
paramClasses: string[];
|
|
7
|
+
paramAttributes: string[];
|
|
8
|
+
paramReset: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare class ApiParamProcessor {
|
|
11
|
+
private readonly imports;
|
|
12
|
+
private readonly classBuilder;
|
|
13
|
+
constructor(params: {
|
|
14
|
+
imports: ModuleImports;
|
|
15
|
+
classBuilder: ModuleClassBuilder;
|
|
16
|
+
});
|
|
17
|
+
process(params: {
|
|
18
|
+
methodName: string;
|
|
19
|
+
querys: AttributeProp[];
|
|
20
|
+
paths: PrimitiveProp[];
|
|
21
|
+
headers: PrimitiveProp[];
|
|
22
|
+
cookies: PrimitiveProp[];
|
|
23
|
+
}): ApiProcessedParams;
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { capitalizeFirstLetter } from "../../helpers/helpers.js";
|
|
2
|
+
export class ApiParamProcessor {
|
|
3
|
+
imports;
|
|
4
|
+
classBuilder;
|
|
5
|
+
constructor(params) {
|
|
6
|
+
this.imports = params.imports;
|
|
7
|
+
this.classBuilder = params.classBuilder;
|
|
8
|
+
}
|
|
9
|
+
process(params) {
|
|
10
|
+
const { methodName, cookies, headers, paths, querys } = params;
|
|
11
|
+
const paramClasses = [];
|
|
12
|
+
const paramAttributes = [];
|
|
13
|
+
const paramReset = [];
|
|
14
|
+
const processParam = (paramData) => {
|
|
15
|
+
const { paramType, attrName, props } = paramData;
|
|
16
|
+
const className = `${capitalizeFirstLetter(methodName)}${capitalizeFirstLetter(paramType)}`;
|
|
17
|
+
paramClasses.push(this.classBuilder.buildClassProps({ props, name: paramType, className }));
|
|
18
|
+
paramAttributes.push(`${attrName} = new ${className}()`);
|
|
19
|
+
paramReset.push(`this.${attrName} = new ${className}()`);
|
|
20
|
+
};
|
|
21
|
+
const argEntries = [
|
|
22
|
+
{ attrName: "querys", props: querys },
|
|
23
|
+
{ attrName: "headers", props: headers },
|
|
24
|
+
{ attrName: "paths", props: paths },
|
|
25
|
+
{ attrName: "cookies", props: cookies },
|
|
26
|
+
];
|
|
27
|
+
for (const { attrName, props } of argEntries) {
|
|
28
|
+
if (!props.length)
|
|
29
|
+
continue;
|
|
30
|
+
this.imports.addReflectorImport("QueryBuilder");
|
|
31
|
+
const paramType = capitalizeFirstLetter(attrName);
|
|
32
|
+
processParam({ paramType, attrName, props });
|
|
33
|
+
}
|
|
34
|
+
return { paramClasses, paramAttributes, paramReset };
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export { ModuleParamProcessor, type ProcessedParams } from "./module/ModuleParam
|
|
|
4
4
|
export { ModuleClassBuilder } from "./module/ModuleClassBuilder.js";
|
|
5
5
|
export { ModuleConstructorBuilder, type Form } from "./module/ModuleConstructorBuilder.js";
|
|
6
6
|
export { ModuleFileBuilder, type FileBuildParams } from "./module/ModuleFileBuilder.js";
|
|
7
|
+
export { ApiFileBuilder } from "./api/ApiFileBuilder.js";
|
|
8
|
+
export { ApiClassBuilder, type ApiEndpointBlock } from "./api/ApiClassBuilder.js";
|
|
9
|
+
export { ApiMethodGenerator } from "./api/ApiMethodGenerator.js";
|
|
10
|
+
export { ApiParamProcessor, type ApiProcessedParams } from "./api/ApiParamProcessor.js";
|
|
7
11
|
export { Method } from "./method/Method.js";
|
|
8
12
|
export { MethodBuilder } from "./method/MethodBuilder.js";
|
|
9
13
|
export { MethodApiTypeAnalyzer } from "./method/MethodApiTypeAnalyzer.js";
|
package/dist/core/index.js
CHANGED
|
@@ -5,6 +5,11 @@ export { ModuleParamProcessor } from "./module/ModuleParamProcessor.js";
|
|
|
5
5
|
export { ModuleClassBuilder } from "./module/ModuleClassBuilder.js";
|
|
6
6
|
export { ModuleConstructorBuilder } from "./module/ModuleConstructorBuilder.js";
|
|
7
7
|
export { ModuleFileBuilder } from "./module/ModuleFileBuilder.js";
|
|
8
|
+
// Api-related exports
|
|
9
|
+
export { ApiFileBuilder } from "./api/ApiFileBuilder.js";
|
|
10
|
+
export { ApiClassBuilder } from "./api/ApiClassBuilder.js";
|
|
11
|
+
export { ApiMethodGenerator } from "./api/ApiMethodGenerator.js";
|
|
12
|
+
export { ApiParamProcessor } from "./api/ApiParamProcessor.js";
|
|
8
13
|
// Method-related exports
|
|
9
14
|
export { Method } from "./method/Method.js";
|
|
10
15
|
export { MethodBuilder } from "./method/MethodBuilder.js";
|
|
@@ -24,10 +24,11 @@ export class MethodBuilder {
|
|
|
24
24
|
apiType,
|
|
25
25
|
parameters: this.requestAnalyzer.paths,
|
|
26
26
|
hasEnumResponse: this.responseAnalyzer.hasEnumResponse,
|
|
27
|
+
isPrimitiveResponse: this.responseAnalyzer.isPrimitiveResponse,
|
|
27
28
|
},
|
|
28
29
|
props,
|
|
29
30
|
};
|
|
30
|
-
const responseTypeInterface = this.buildResponseTypeInterface(analyzers.request.responseType);
|
|
31
|
+
const responseTypeInterface = this.buildResponseTypeInterface(analyzers.request.responseType, analyzers.request.isPrimitiveResponse);
|
|
31
32
|
return new Method({
|
|
32
33
|
name,
|
|
33
34
|
endpoint: operation.endpoint,
|
|
@@ -44,7 +45,11 @@ export class MethodBuilder {
|
|
|
44
45
|
return "listAll";
|
|
45
46
|
return extracted ?? this.apiTypeAnalyzer.analyze(operation).apiType;
|
|
46
47
|
}
|
|
47
|
-
buildResponseTypeInterface(responseType) {
|
|
48
|
-
|
|
48
|
+
buildResponseTypeInterface(responseType, isPrimitiveResponse) {
|
|
49
|
+
if (!responseType)
|
|
50
|
+
return "null";
|
|
51
|
+
if (isPrimitiveResponse)
|
|
52
|
+
return responseType;
|
|
53
|
+
return `${responseType}Interface`;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
@@ -2,7 +2,9 @@ import type { ResponsesObject } from "../../types/open-api-spec.interface.js";
|
|
|
2
2
|
export declare class MethodResponseAnalyzer {
|
|
3
3
|
responseType: string | null;
|
|
4
4
|
hasEnumResponse: boolean;
|
|
5
|
+
isPrimitiveResponse: boolean;
|
|
5
6
|
analyze(responses: ResponsesObject): void;
|
|
7
|
+
private normalizePrimitive;
|
|
6
8
|
private isRef;
|
|
7
9
|
private componentName;
|
|
8
10
|
private getFromContent;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
const PRIMITIVE_RESPONSE_TYPES = new Set(["string", "number", "boolean", "integer", "any", "object", "array"]);
|
|
1
2
|
export class MethodResponseAnalyzer {
|
|
2
3
|
responseType = null;
|
|
3
4
|
hasEnumResponse = false;
|
|
5
|
+
isPrimitiveResponse = false;
|
|
4
6
|
analyze(responses) {
|
|
5
7
|
for (const response of Object.values(responses)) {
|
|
6
8
|
if (!response || this.isRef(response))
|
|
@@ -8,11 +10,21 @@ export class MethodResponseAnalyzer {
|
|
|
8
10
|
const schemaOrType = this.getFromContent(response.content);
|
|
9
11
|
const type = this.typeFromSchemaOrType(schemaOrType);
|
|
10
12
|
if (type !== undefined) {
|
|
11
|
-
this.
|
|
13
|
+
this.isPrimitiveResponse = PRIMITIVE_RESPONSE_TYPES.has(type);
|
|
14
|
+
this.responseType = this.normalizePrimitive(type);
|
|
12
15
|
break;
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
}
|
|
19
|
+
normalizePrimitive(type) {
|
|
20
|
+
if (type === "integer")
|
|
21
|
+
return "number";
|
|
22
|
+
if (type === "array")
|
|
23
|
+
return "unknown[]";
|
|
24
|
+
if (type === "object")
|
|
25
|
+
return "unknown";
|
|
26
|
+
return type;
|
|
27
|
+
}
|
|
16
28
|
isRef(v) {
|
|
17
29
|
return !!v && typeof v === "object" && "$ref" in v;
|
|
18
30
|
}
|
|
@@ -46,11 +58,14 @@ export class MethodResponseAnalyzer {
|
|
|
46
58
|
this.hasEnumResponse = true;
|
|
47
59
|
return this.extractEnumType(schema);
|
|
48
60
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
if (schema.allOf) {
|
|
62
|
+
for (const entry of schema.allOf) {
|
|
63
|
+
if (this.isRef(entry))
|
|
64
|
+
continue;
|
|
65
|
+
const t = this.typeFromProperties(entry.properties);
|
|
66
|
+
if (t !== undefined)
|
|
67
|
+
return t;
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
if (schema.type === "array" && schema.items) {
|
|
56
71
|
const items = schema.items;
|
|
@@ -31,9 +31,10 @@ export class MethodApiCallBuilder {
|
|
|
31
31
|
}
|
|
32
32
|
buildEntityCall(method, responseType) {
|
|
33
33
|
const rType = method.analyzers.request.responseType;
|
|
34
|
+
const isPrimitive = method.analyzers.request.isPrimitiveResponse;
|
|
34
35
|
const querys = this.buildQuerys(method.analyzers.props.querys);
|
|
35
36
|
const entityName = treatByUppercase(rType ?? "");
|
|
36
|
-
const buildedThisResponseType = rType
|
|
37
|
+
const buildedThisResponseType = rType && !isPrimitive
|
|
37
38
|
? `this.${entityName} = new ${method.analyzers.request.responseType}({ data: response })`
|
|
38
39
|
: "";
|
|
39
40
|
const inside = `
|
|
@@ -62,7 +62,7 @@ export class MethodGenerator {
|
|
|
62
62
|
return `/** ${method.description ?? ""} */`;
|
|
63
63
|
}
|
|
64
64
|
buildMethodReturn(method) {
|
|
65
|
-
const { attributeType, responseType, hasEnumResponse } = method.analyzers.request;
|
|
65
|
+
const { attributeType, responseType, hasEnumResponse, isPrimitiveResponse } = method.analyzers.request;
|
|
66
66
|
if (attributeType === "list") {
|
|
67
67
|
return "this.list";
|
|
68
68
|
}
|
|
@@ -72,6 +72,9 @@ export class MethodGenerator {
|
|
|
72
72
|
if (hasEnumResponse) {
|
|
73
73
|
return "response.data";
|
|
74
74
|
}
|
|
75
|
+
if (isPrimitiveResponse) {
|
|
76
|
+
return "response";
|
|
77
|
+
}
|
|
75
78
|
return `new ${responseType}({ data: response })`;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -5,7 +5,8 @@ export class ModuleClassBuilder {
|
|
|
5
5
|
this.imports = params.imports;
|
|
6
6
|
}
|
|
7
7
|
buildClassProps(params) {
|
|
8
|
-
const { name, props } = params;
|
|
8
|
+
const { name, props, className } = params;
|
|
9
|
+
const outputName = className ?? name;
|
|
9
10
|
const bundle = [];
|
|
10
11
|
const attributes = [];
|
|
11
12
|
if (name === "Paths") {
|
|
@@ -18,7 +19,7 @@ export class ModuleClassBuilder {
|
|
|
18
19
|
});
|
|
19
20
|
this.imports.addPageStateImport();
|
|
20
21
|
return `
|
|
21
|
-
class ${
|
|
22
|
+
class ${outputName} {
|
|
22
23
|
${attributes.join(";")}
|
|
23
24
|
}
|
|
24
25
|
`;
|
|
@@ -66,7 +67,7 @@ export class ModuleClassBuilder {
|
|
|
66
67
|
}
|
|
67
68
|
`;
|
|
68
69
|
return `
|
|
69
|
-
class ${
|
|
70
|
+
class ${outputName} {
|
|
70
71
|
${attributes.join(";")}
|
|
71
72
|
|
|
72
73
|
${constructorBuild}
|
|
@@ -100,7 +101,7 @@ export class ModuleClassBuilder {
|
|
|
100
101
|
`
|
|
101
102
|
: "";
|
|
102
103
|
return `
|
|
103
|
-
class ${
|
|
104
|
+
class ${outputName} {
|
|
104
105
|
${attributes.join(";")}
|
|
105
106
|
|
|
106
107
|
${bundleBuild}
|
|
@@ -16,6 +16,7 @@ export declare class ModuleImports {
|
|
|
16
16
|
hasEnumImports(): boolean;
|
|
17
17
|
private hasMockedImports;
|
|
18
18
|
getMockedImports(): string[];
|
|
19
|
+
private readonly typeOnlyImports;
|
|
19
20
|
buildReflectorImportsLine(): string;
|
|
20
21
|
buildEnumImportsLine(): string;
|
|
21
22
|
buildMockedImportsLine(): string;
|
|
@@ -50,11 +50,16 @@ export class ModuleImports {
|
|
|
50
50
|
getMockedImports() {
|
|
51
51
|
return Array.from(this.mockedImports);
|
|
52
52
|
}
|
|
53
|
+
typeOnlyImports = new Set(["ApiCallParams"]);
|
|
53
54
|
buildReflectorImportsLine() {
|
|
54
55
|
const imports = this.getReflectorImportsArray();
|
|
55
|
-
const regularImports = imports.filter(i => i !== "setQueryGroup");
|
|
56
|
+
const regularImports = imports.filter(i => i !== "setQueryGroup" && !this.typeOnlyImports.has(i));
|
|
57
|
+
const typeImports = imports.filter(i => this.typeOnlyImports.has(i));
|
|
56
58
|
const hasSetQueryGroup = imports.includes("setQueryGroup");
|
|
57
59
|
let result = `import { ${regularImports.join(", ")}, type ApiErrorResponse`;
|
|
60
|
+
for (const t of typeImports) {
|
|
61
|
+
result += `, type ${t}`;
|
|
62
|
+
}
|
|
58
63
|
if (hasSetQueryGroup) {
|
|
59
64
|
result += `, setQueryGroup`;
|
|
60
65
|
}
|
|
@@ -7,21 +7,21 @@ export class ModuleMethodProcessor {
|
|
|
7
7
|
shouldSkipMethod(method) {
|
|
8
8
|
const { bodyType, responseType, attributeType } = method.request;
|
|
9
9
|
if (bodyType === "string") {
|
|
10
|
-
createDangerMessage(`
|
|
10
|
+
createDangerMessage(`Method ${method.name} was skipped because it has an invalid body.`);
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
13
|
const isNullResponse = !responseType || responseType === "null";
|
|
14
14
|
if ((attributeType === "entity" || attributeType === "list") && isNullResponse) {
|
|
15
|
-
createDangerMessage(`
|
|
15
|
+
createDangerMessage(`Method ${method.name} was skipped because it has a null response.`);
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
|
-
//
|
|
18
|
+
// Check that all endpoint path params are declared as parameters
|
|
19
19
|
const endpointParams = [...method.endpoint.matchAll(/\{(\w+)\}/g)].map((m) => m[1]).filter(Boolean);
|
|
20
20
|
if (endpointParams.length > 0) {
|
|
21
21
|
const declaredPaths = new Set(method.paths.map((p) => p.name));
|
|
22
22
|
const undeclared = endpointParams.filter((p) => !declaredPaths.has(p));
|
|
23
23
|
if (undeclared.length > 0) {
|
|
24
|
-
createDangerMessage(`
|
|
24
|
+
createDangerMessage(`Method ${method.name} was skipped because it has undeclared path params: ${undeclared.join(", ")}`);
|
|
25
25
|
return true;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -42,7 +42,7 @@ export class ModuleMethodProcessor {
|
|
|
42
42
|
const cookieMap = new Map();
|
|
43
43
|
for (const method of methods) {
|
|
44
44
|
const { request, headers, cookies, paths, querys } = method;
|
|
45
|
-
const { bodyType, responseType, attributeType } = request;
|
|
45
|
+
const { bodyType, responseType, attributeType, isPrimitiveResponse } = request;
|
|
46
46
|
headers.forEach((h) => headerMap.set(h.name, h));
|
|
47
47
|
cookies.forEach((c) => cookieMap.set(c.name, c));
|
|
48
48
|
paths.forEach((p) => pathMap.set(p.name, p));
|
|
@@ -57,7 +57,7 @@ export class ModuleMethodProcessor {
|
|
|
57
57
|
this.imports.addReflectorImport("isFormValid");
|
|
58
58
|
}
|
|
59
59
|
buildedMethods.push(method.build());
|
|
60
|
-
if (attributeType === "entity") {
|
|
60
|
+
if (attributeType === "entity" && !isPrimitiveResponse) {
|
|
61
61
|
const entityName = treatByUppercase(method.request.responseType ?? "");
|
|
62
62
|
methodsAttributes.add(`${entityName} = $state<${responseType} | undefined>()`);
|
|
63
63
|
methodsInit.add(`this.clear${capitalizeFirstLetter(entityName)}()`);
|
|
@@ -74,7 +74,7 @@ export class ModuleMethodProcessor {
|
|
|
74
74
|
if (bodyType) {
|
|
75
75
|
entries.add(bodyType);
|
|
76
76
|
}
|
|
77
|
-
if (responseType && responseType !== "response") {
|
|
77
|
+
if (responseType && responseType !== "response" && !isPrimitiveResponse) {
|
|
78
78
|
entries.add(`type ${responseType}Interface`);
|
|
79
79
|
entries.add(responseType);
|
|
80
80
|
}
|
package/dist/generate-doc.js
CHANGED
|
@@ -84,12 +84,16 @@ export async function reflector(manual = false) {
|
|
|
84
84
|
}
|
|
85
85
|
// Lê reflector.json do projeto consumidor (opcional)
|
|
86
86
|
let apiImport = "$lib/api";
|
|
87
|
+
let experimentalFeatures = false;
|
|
87
88
|
try {
|
|
88
89
|
const reflectorJsonPath = path.resolve(process.cwd(), "reflector.json");
|
|
89
90
|
const reflectorJson = JSON.parse(fs.readFileSync(reflectorJsonPath, "utf8"));
|
|
90
91
|
if (reflectorJson.api) {
|
|
91
92
|
apiImport = reflectorJson.api;
|
|
92
93
|
}
|
|
94
|
+
if (reflectorJson.experimentalFeatures === true) {
|
|
95
|
+
experimentalFeatures = true;
|
|
96
|
+
}
|
|
93
97
|
}
|
|
94
98
|
catch {
|
|
95
99
|
// reflector.json não encontrado ou inválido — usa o padrão
|
|
@@ -99,7 +103,7 @@ export async function reflector(manual = false) {
|
|
|
99
103
|
console.warn("[reflector] OpenAPI sem components; abortando.");
|
|
100
104
|
return breakReflector();
|
|
101
105
|
}
|
|
102
|
-
const r = new Reflector({ components, paths, fieldConfigs, typeImports, apiImport });
|
|
106
|
+
const r = new Reflector({ components, paths, fieldConfigs, typeImports, apiImport, experimentalFeatures });
|
|
103
107
|
await r.build();
|
|
104
108
|
await r.localSave(data);
|
|
105
109
|
return breakReflector();
|
package/dist/main.d.ts
CHANGED
|
@@ -21,13 +21,44 @@ export declare class Reflector {
|
|
|
21
21
|
schemas: Schema[];
|
|
22
22
|
modules: Module[];
|
|
23
23
|
readonly apiImport: string;
|
|
24
|
+
readonly experimentalFeatures: boolean;
|
|
24
25
|
constructor(params: {
|
|
25
26
|
components: ComponentsObject;
|
|
26
27
|
paths: PathsObject;
|
|
27
28
|
fieldConfigs: FieldConfigs;
|
|
28
29
|
typeImports: TypeImports;
|
|
29
30
|
apiImport: string;
|
|
31
|
+
experimentalFeatures?: boolean;
|
|
30
32
|
});
|
|
33
|
+
/**
|
|
34
|
+
* Promotes inline object request-body schemas to named components.
|
|
35
|
+
* Without this, `MethodBodyAnalyzer` falls back to `schema.type` ("object")
|
|
36
|
+
* because inline bodies aren't referenced by a `$ref`, and the resulting
|
|
37
|
+
* body type name collides with the JS primitive — so the generated module
|
|
38
|
+
* imports a non-existent `{ object }` class and the schema file is never
|
|
39
|
+
* emitted (resolveTransitiveDeps finds nothing in schemaMap).
|
|
40
|
+
*/
|
|
41
|
+
private extractInlineBodies;
|
|
42
|
+
private promoteInlineBody;
|
|
43
|
+
/**
|
|
44
|
+
* Promotes inline response `data` schemas to named components.
|
|
45
|
+
* Responses in this API follow a `{success, data, message}` envelope where
|
|
46
|
+
* `data` is either declared directly or overlaid via `allOf`. When `data` is
|
|
47
|
+
* an inline object/array (no `$ref`), the response analyzer can only
|
|
48
|
+
* extract the raw `schema.type` ("object"/"array"), which isn't a valid
|
|
49
|
+
* class name. Promoting `data` to a named component gives the analyzer a
|
|
50
|
+
* proper `$ref` to resolve.
|
|
51
|
+
*/
|
|
52
|
+
private extractInlineResponses;
|
|
53
|
+
private promoteInlineResponse;
|
|
54
|
+
private findSuccessResponse;
|
|
55
|
+
/**
|
|
56
|
+
* Locates the inline `data` sub-schema inside a response envelope, whether
|
|
57
|
+
* it lives at `schema.properties.data` or inside an `allOf` entry. Returns
|
|
58
|
+
* a handle that lets the caller swap the inline schema for a `$ref`.
|
|
59
|
+
*/
|
|
60
|
+
private findInlineDataHolder;
|
|
61
|
+
private hasExtractableContent;
|
|
31
62
|
private getSchemas;
|
|
32
63
|
private getModules;
|
|
33
64
|
build(): Promise<{}>;
|
package/dist/main.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { Source } from "./file.js";
|
|
4
|
-
import { getEndpoint, isReferenceObject, splitByUppercase } from "./helpers/helpers.js";
|
|
4
|
+
import { capitalizeFirstLetter, getEndpoint, isReferenceObject, splitByUppercase } from "./helpers/helpers.js";
|
|
5
5
|
import { Schema } from "./schema.js";
|
|
6
6
|
import { Module } from "./module.js";
|
|
7
7
|
import { generatedDir } from "./vars.global.js";
|
|
8
8
|
import { ReflectorFile } from "./reflector.js";
|
|
9
|
+
import * as process from "node:process";
|
|
9
10
|
export const enumTypes = new Map();
|
|
10
11
|
export const mockedParams = new Set();
|
|
11
12
|
export class Reflector {
|
|
@@ -24,22 +25,160 @@ export class Reflector {
|
|
|
24
25
|
schemas;
|
|
25
26
|
modules;
|
|
26
27
|
apiImport;
|
|
28
|
+
experimentalFeatures;
|
|
27
29
|
constructor(params) {
|
|
28
|
-
const { components, paths, fieldConfigs, typeImports, apiImport } = params;
|
|
30
|
+
const { components, paths, fieldConfigs, typeImports, apiImport, experimentalFeatures } = params;
|
|
29
31
|
this.apiImport = apiImport;
|
|
32
|
+
this.experimentalFeatures = experimentalFeatures ?? false;
|
|
30
33
|
this.typeImports = typeImports;
|
|
31
|
-
//
|
|
34
|
+
// Clear global state between runs
|
|
32
35
|
enumTypes.clear();
|
|
33
36
|
mockedParams.clear();
|
|
34
37
|
this.clearSrc();
|
|
35
38
|
this.components = components;
|
|
36
39
|
this.paths = paths;
|
|
40
|
+
this.extractInlineBodies();
|
|
41
|
+
this.extractInlineResponses();
|
|
37
42
|
this.files = [];
|
|
38
43
|
this.modules = this.getModules();
|
|
39
44
|
const { propertiesNames, schemas } = this.getSchemas({ fieldConfigs });
|
|
40
45
|
this.propertiesNames = propertiesNames;
|
|
41
46
|
this.schemas = schemas;
|
|
42
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Promotes inline object request-body schemas to named components.
|
|
50
|
+
* Without this, `MethodBodyAnalyzer` falls back to `schema.type` ("object")
|
|
51
|
+
* because inline bodies aren't referenced by a `$ref`, and the resulting
|
|
52
|
+
* body type name collides with the JS primitive — so the generated module
|
|
53
|
+
* imports a non-existent `{ object }` class and the schema file is never
|
|
54
|
+
* emitted (resolveTransitiveDeps finds nothing in schemaMap).
|
|
55
|
+
*/
|
|
56
|
+
extractInlineBodies() {
|
|
57
|
+
this.components.schemas ??= {};
|
|
58
|
+
const schemas = this.components.schemas;
|
|
59
|
+
const usedNames = new Set(Object.keys(schemas));
|
|
60
|
+
const httpMethods = ["get", "put", "post", "delete", "patch", "options", "head", "trace"];
|
|
61
|
+
for (const [pathStr, pathItem] of Object.entries(this.paths)) {
|
|
62
|
+
if (!pathItem)
|
|
63
|
+
continue;
|
|
64
|
+
for (const httpMethod of httpMethods) {
|
|
65
|
+
this.promoteInlineBody(pathItem[httpMethod], httpMethod, pathStr, schemas, usedNames);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
promoteInlineBody(operation, httpMethod, pathStr, schemas, usedNames) {
|
|
70
|
+
if (!operation?.requestBody || "$ref" in operation.requestBody)
|
|
71
|
+
return;
|
|
72
|
+
const firstEntry = Object.values(operation.requestBody.content ?? {})[0];
|
|
73
|
+
const schema = firstEntry?.schema;
|
|
74
|
+
if (!schema || "$ref" in schema || !schema.properties)
|
|
75
|
+
return;
|
|
76
|
+
const base = operation.operationId
|
|
77
|
+
? capitalizeFirstLetter(operation.operationId)
|
|
78
|
+
: capitalizeFirstLetter(httpMethod) + pathStr.replaceAll(/[^a-zA-Z0-9]/g, "");
|
|
79
|
+
let name = `${base}Body`;
|
|
80
|
+
let suffix = 2;
|
|
81
|
+
while (usedNames.has(name)) {
|
|
82
|
+
name = `${base}Body${suffix++}`;
|
|
83
|
+
}
|
|
84
|
+
usedNames.add(name);
|
|
85
|
+
schemas[name] = schema;
|
|
86
|
+
firstEntry.schema = { $ref: `#/components/schemas/${name}` };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Promotes inline response `data` schemas to named components.
|
|
90
|
+
* Responses in this API follow a `{success, data, message}` envelope where
|
|
91
|
+
* `data` is either declared directly or overlaid via `allOf`. When `data` is
|
|
92
|
+
* an inline object/array (no `$ref`), the response analyzer can only
|
|
93
|
+
* extract the raw `schema.type` ("object"/"array"), which isn't a valid
|
|
94
|
+
* class name. Promoting `data` to a named component gives the analyzer a
|
|
95
|
+
* proper `$ref` to resolve.
|
|
96
|
+
*/
|
|
97
|
+
extractInlineResponses() {
|
|
98
|
+
this.components.schemas ??= {};
|
|
99
|
+
const schemas = this.components.schemas;
|
|
100
|
+
const usedNames = new Set(Object.keys(schemas));
|
|
101
|
+
const httpMethods = ["get", "put", "post", "delete", "patch", "options", "head", "trace"];
|
|
102
|
+
for (const [pathStr, pathItem] of Object.entries(this.paths)) {
|
|
103
|
+
if (!pathItem)
|
|
104
|
+
continue;
|
|
105
|
+
for (const httpMethod of httpMethods) {
|
|
106
|
+
this.promoteInlineResponse(pathItem[httpMethod], httpMethod, pathStr, schemas, usedNames);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
promoteInlineResponse(operation, httpMethod, pathStr, schemas, usedNames) {
|
|
111
|
+
if (!operation?.responses)
|
|
112
|
+
return;
|
|
113
|
+
const successResponse = this.findSuccessResponse(operation.responses);
|
|
114
|
+
if (!successResponse || "$ref" in successResponse || !successResponse.content)
|
|
115
|
+
return;
|
|
116
|
+
const firstEntry = Object.values(successResponse.content)[0];
|
|
117
|
+
const schema = firstEntry?.schema;
|
|
118
|
+
if (!schema || "$ref" in schema)
|
|
119
|
+
return;
|
|
120
|
+
const dataHolder = this.findInlineDataHolder(schema);
|
|
121
|
+
if (!dataHolder)
|
|
122
|
+
return;
|
|
123
|
+
const base = operation.operationId
|
|
124
|
+
? capitalizeFirstLetter(operation.operationId)
|
|
125
|
+
: capitalizeFirstLetter(httpMethod) + pathStr.replaceAll(/[^a-zA-Z0-9]/g, "");
|
|
126
|
+
let name = `${base}Response`;
|
|
127
|
+
let suffix = 2;
|
|
128
|
+
while (usedNames.has(name)) {
|
|
129
|
+
name = `${base}Response${suffix++}`;
|
|
130
|
+
}
|
|
131
|
+
usedNames.add(name);
|
|
132
|
+
schemas[name] = dataHolder.data;
|
|
133
|
+
dataHolder.replace({ $ref: `#/components/schemas/${name}` });
|
|
134
|
+
}
|
|
135
|
+
findSuccessResponse(responses) {
|
|
136
|
+
for (const [code, response] of Object.entries(responses)) {
|
|
137
|
+
if (code.startsWith("2"))
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Locates the inline `data` sub-schema inside a response envelope, whether
|
|
144
|
+
* it lives at `schema.properties.data` or inside an `allOf` entry. Returns
|
|
145
|
+
* a handle that lets the caller swap the inline schema for a `$ref`.
|
|
146
|
+
*/
|
|
147
|
+
findInlineDataHolder(schema) {
|
|
148
|
+
const holders = [];
|
|
149
|
+
if (schema.properties)
|
|
150
|
+
holders.push(schema);
|
|
151
|
+
if (schema.allOf) {
|
|
152
|
+
for (const entry of schema.allOf) {
|
|
153
|
+
if (!("$ref" in entry) && entry.properties) {
|
|
154
|
+
holders.push(entry);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const holder of holders) {
|
|
159
|
+
const data = holder.properties?.["data"];
|
|
160
|
+
if (!data || "$ref" in data)
|
|
161
|
+
continue;
|
|
162
|
+
if (!this.hasExtractableContent(data))
|
|
163
|
+
continue;
|
|
164
|
+
return {
|
|
165
|
+
data,
|
|
166
|
+
replace: (ref) => {
|
|
167
|
+
holder.properties["data"] = ref;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
hasExtractableContent(schema) {
|
|
174
|
+
if (schema.properties)
|
|
175
|
+
return true;
|
|
176
|
+
if (schema.items)
|
|
177
|
+
return true;
|
|
178
|
+
if (schema.allOf?.length)
|
|
179
|
+
return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
43
182
|
getSchemas(params) {
|
|
44
183
|
const { fieldConfigs } = params;
|
|
45
184
|
const componentSchemas = this.components.schemas;
|
|
@@ -65,7 +204,7 @@ export class Reflector {
|
|
|
65
204
|
for (const schema of schemas) {
|
|
66
205
|
this.schemaMap.set(schema.name, schema);
|
|
67
206
|
}
|
|
68
|
-
console.log(`${schemas.length} schemas
|
|
207
|
+
console.log(`${schemas.length} schemas generated successfully.`);
|
|
69
208
|
return { schemas, propertiesNames };
|
|
70
209
|
}
|
|
71
210
|
getModules() {
|
|
@@ -73,8 +212,8 @@ export class Reflector {
|
|
|
73
212
|
for (const [path, methods] of Object.entries(this.paths)) {
|
|
74
213
|
const rawMethod = Object.values(methods)[0];
|
|
75
214
|
const rawName = rawMethod.operationId?.split("_")[0] ?? "";
|
|
76
|
-
const
|
|
77
|
-
const moduleName =
|
|
215
|
+
const nameParts = splitByUppercase(rawName).filter((x) => x !== "Controller");
|
|
216
|
+
const moduleName = nameParts.join("");
|
|
78
217
|
const baseEndpoint = getEndpoint(path);
|
|
79
218
|
const operations = Object.entries(methods).map(([apiMethod, attributes]) => {
|
|
80
219
|
return {
|
|
@@ -101,6 +240,7 @@ export class Reflector {
|
|
|
101
240
|
name,
|
|
102
241
|
...info,
|
|
103
242
|
apiImport: this.apiImport,
|
|
243
|
+
experimentalFeatures: this.experimentalFeatures,
|
|
104
244
|
});
|
|
105
245
|
});
|
|
106
246
|
return modules;
|
|
@@ -146,7 +286,7 @@ export class Reflector {
|
|
|
146
286
|
export default mockedParams
|
|
147
287
|
`;
|
|
148
288
|
this.mockedParamsFile.changeData(mockedFile);
|
|
149
|
-
//
|
|
289
|
+
// Save all files in parallel, awaiting completion
|
|
150
290
|
await Promise.all([
|
|
151
291
|
...moduleSchemaFiles.map((f) => f.save()),
|
|
152
292
|
this.typesSrc.save(),
|
|
@@ -154,6 +294,7 @@ export class Reflector {
|
|
|
154
294
|
this.enumFile.save(),
|
|
155
295
|
this.mockedParamsFile.save(),
|
|
156
296
|
...this.modules.filter((m) => m.methods.length > 0).map((m) => m.src.save()),
|
|
297
|
+
...this.modules.filter((m) => m.methods.length > 0 && m.apiSrc).map((m) => m.apiSrc.save()),
|
|
157
298
|
]);
|
|
158
299
|
return {};
|
|
159
300
|
}
|
package/dist/method.d.ts
CHANGED
|
@@ -15,7 +15,10 @@ export declare class Method {
|
|
|
15
15
|
apiType: import("./types/types.js").ApiType;
|
|
16
16
|
parameters: any[];
|
|
17
17
|
hasEnumResponse: boolean;
|
|
18
|
+
isPrimitiveResponse: boolean;
|
|
18
19
|
};
|
|
20
|
+
get analyzers(): import("./core/method/Method.js").MethodAnalyzers;
|
|
21
|
+
get responseTypeInterface(): string;
|
|
19
22
|
get headers(): PrimitiveProp[];
|
|
20
23
|
get cookies(): PrimitiveProp[];
|
|
21
24
|
get paths(): PrimitiveProp[];
|
package/dist/method.js
CHANGED
|
@@ -11,6 +11,12 @@ export class Method {
|
|
|
11
11
|
get request() {
|
|
12
12
|
return this.method.analyzers.request;
|
|
13
13
|
}
|
|
14
|
+
get analyzers() {
|
|
15
|
+
return this.method.analyzers;
|
|
16
|
+
}
|
|
17
|
+
get responseTypeInterface() {
|
|
18
|
+
return this.method.responseTypeInterface;
|
|
19
|
+
}
|
|
14
20
|
get headers() {
|
|
15
21
|
return this.method.analyzers.props.headers;
|
|
16
22
|
}
|
package/dist/module.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare class Module {
|
|
|
6
6
|
readonly path: string;
|
|
7
7
|
readonly moduleName: string;
|
|
8
8
|
readonly src: Source;
|
|
9
|
+
readonly apiSrc: Source | null;
|
|
9
10
|
readonly methods: Method[];
|
|
10
11
|
/** Schema class names directly used by this module (for per-module schema generation) */
|
|
11
12
|
readonly schemaClassNames: string[];
|
|
@@ -21,7 +22,10 @@ export declare class Module {
|
|
|
21
22
|
operations: ReflectorOperation[];
|
|
22
23
|
path: string;
|
|
23
24
|
apiImport: string;
|
|
25
|
+
experimentalFeatures?: boolean;
|
|
24
26
|
});
|
|
25
27
|
private buildModuleData;
|
|
28
|
+
private buildApiFile;
|
|
29
|
+
private getApiPath;
|
|
26
30
|
private getPath;
|
|
27
31
|
}
|
package/dist/module.js
CHANGED
|
@@ -4,12 +4,13 @@ import { Source } from "./file.js";
|
|
|
4
4
|
import { capitalizeFirstLetter, toKebabCase } from "./helpers/helpers.js";
|
|
5
5
|
import { Method } from "./method.js";
|
|
6
6
|
import { generatedDir } from "./vars.global.js";
|
|
7
|
-
import { ModuleImports, ModuleMethodProcessor, ModuleParamProcessor, ModuleClassBuilder, ModuleConstructorBuilder, ModuleFileBuilder, } from "./core/index.js";
|
|
7
|
+
import { ModuleImports, ModuleMethodProcessor, ModuleParamProcessor, ModuleClassBuilder, ModuleConstructorBuilder, ModuleFileBuilder, ApiFileBuilder, ApiClassBuilder, } from "./core/index.js";
|
|
8
8
|
export class Module {
|
|
9
9
|
name;
|
|
10
10
|
path;
|
|
11
11
|
moduleName;
|
|
12
12
|
src;
|
|
13
|
+
apiSrc;
|
|
13
14
|
methods;
|
|
14
15
|
/** Schema class names directly used by this module (for per-module schema generation) */
|
|
15
16
|
schemaClassNames;
|
|
@@ -20,7 +21,7 @@ export class Module {
|
|
|
20
21
|
constructorBuilder;
|
|
21
22
|
fileBuilder;
|
|
22
23
|
constructor(params) {
|
|
23
|
-
const { name, operations, moduleName, path: modulePath, apiImport } = params;
|
|
24
|
+
const { name, operations, moduleName, path: modulePath, apiImport, experimentalFeatures } = params;
|
|
24
25
|
this.moduleName = moduleName;
|
|
25
26
|
this.name = capitalizeFirstLetter(name);
|
|
26
27
|
this.path = modulePath;
|
|
@@ -58,9 +59,11 @@ export class Module {
|
|
|
58
59
|
data: this.fileBuilder.build({
|
|
59
60
|
...allBuilded,
|
|
60
61
|
moduleConstructor,
|
|
61
|
-
moduleName: this.
|
|
62
|
+
moduleName: this.name,
|
|
62
63
|
}),
|
|
63
64
|
});
|
|
65
|
+
// Cria o arquivo Api (apenas com experimentalFeatures)
|
|
66
|
+
this.apiSrc = experimentalFeatures ? this.buildApiFile(apiImport) : null;
|
|
64
67
|
}
|
|
65
68
|
buildModuleData(processedMethods, processedParams) {
|
|
66
69
|
const moduleAttributes = new Set().add("loading = $state<boolean>(false)");
|
|
@@ -102,6 +105,36 @@ export class Module {
|
|
|
102
105
|
buildedMethods,
|
|
103
106
|
};
|
|
104
107
|
}
|
|
108
|
+
buildApiFile(apiImport) {
|
|
109
|
+
const apiImports = new ModuleImports(apiImport);
|
|
110
|
+
apiImports.addReflectorImport("ApiCallParams");
|
|
111
|
+
const apiClassBuilderDep = new ModuleClassBuilder({ imports: apiImports });
|
|
112
|
+
const apiClassBuilder = new ApiClassBuilder({ imports: apiImports, classBuilder: apiClassBuilderDep });
|
|
113
|
+
const apiFileBuilder = new ApiFileBuilder({ imports: apiImports });
|
|
114
|
+
const endpointBlocks = [];
|
|
115
|
+
const apiSchemaEntries = new Set();
|
|
116
|
+
for (const method of this.methods) {
|
|
117
|
+
const block = apiClassBuilder.build({ method });
|
|
118
|
+
if (!block)
|
|
119
|
+
continue;
|
|
120
|
+
endpointBlocks.push(block);
|
|
121
|
+
block.schemaEntries.forEach((e) => apiSchemaEntries.add(e));
|
|
122
|
+
}
|
|
123
|
+
const cleanEntries = Array.from(apiSchemaEntries).filter((x) => x !== "type any");
|
|
124
|
+
const kebabName = toKebabCase(this.name);
|
|
125
|
+
const classImports = cleanEntries.length > 0
|
|
126
|
+
? `import { ${cleanEntries.join(", ")} } from './${kebabName}.schema.svelte';`
|
|
127
|
+
: "";
|
|
128
|
+
return new Source({
|
|
129
|
+
path: this.getApiPath(),
|
|
130
|
+
data: apiFileBuilder.build({ endpointBlocks, classImports }),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
getApiPath() {
|
|
134
|
+
const kebabName = toKebabCase(this.name);
|
|
135
|
+
const inPath = path.join(generatedDir, "controllers", kebabName);
|
|
136
|
+
return path.join(inPath, `${kebabName}.api.svelte.ts`);
|
|
137
|
+
}
|
|
105
138
|
getPath() {
|
|
106
139
|
const kebabName = toKebabCase(this.name);
|
|
107
140
|
const inPath = path.join(generatedDir, "controllers", kebabName);
|
|
@@ -22,7 +22,8 @@ export class PrimitiveProp {
|
|
|
22
22
|
const { name, schemaObject, required, validator, customType, isParam, isNullable } = params;
|
|
23
23
|
const { type: rawType } = schemaObject;
|
|
24
24
|
this.isNullable = !!isNullable;
|
|
25
|
-
const
|
|
25
|
+
const normalizedRawType = rawType === "integer" ? "number" : rawType;
|
|
26
|
+
const type = normalizedRawType ?? "string";
|
|
26
27
|
const { emptyExample, example } = this.getExampleAndFallback({ schemaObject, type, name });
|
|
27
28
|
this.example = example;
|
|
28
29
|
this.fallbackExample = emptyExample;
|
package/dist/reflector.js
CHANGED
|
@@ -10,6 +10,10 @@ export class ReflectorFile {
|
|
|
10
10
|
"type ValidatorResult = string | null",
|
|
11
11
|
"type ValidatorFn<T> = (v: T) => ValidatorResult",
|
|
12
12
|
"type BundleResult<T> = T extends { bundle: () => infer R } ? R : T;",
|
|
13
|
+
`export type ApiCallParams<TResponse, TPaths = void> =
|
|
14
|
+
TPaths extends void
|
|
15
|
+
? { behavior?: Behavior<TResponse, ApiErrorResponse> }
|
|
16
|
+
: { behavior?: Behavior<TResponse, ApiErrorResponse>; paths?: TPaths }`,
|
|
13
17
|
`type PartialBuildedInput<T> = {
|
|
14
18
|
[K in Exclude<keyof T, 'bundle'>]?: BuildedInput<T[K]>;
|
|
15
19
|
} & {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-reflector",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Reflects zod types from openAPI schemas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dotenv": "^16.4.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^24.12.
|
|
28
|
+
"@types/node": "^24.12.2",
|
|
29
29
|
"typescript": "^5.9.3"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|