@goast/kotlin 0.5.13-beta.1 → 0.5.14
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/LICENSE +21 -21
- package/README.md +45 -45
- package/assets/client/okhttp3/ApiAbstractions.kt +30 -30
- package/assets/client/okhttp3/ApiClient.kt +252 -252
- package/assets/client/okhttp3/ApiResponse.kt +43 -43
- package/assets/client/okhttp3/Errors.kt +21 -21
- package/assets/client/okhttp3/PartConfig.kt +11 -11
- package/assets/client/okhttp3/RequestConfig.kt +18 -18
- package/assets/client/okhttp3/RequestMethod.kt +8 -8
- package/assets/client/okhttp3/ResponseExtensions.kt +24 -24
- package/assets/client/spring-reactive-web-clients/ApiRequestFile.kt +33 -33
- package/esm/src/generators/services/okhttp3-clients/okhttp3-client-generator.js +1 -1
- package/esm/src/generators/services/spring-controllers/spring-controller-generator.d.ts.map +1 -1
- package/esm/src/generators/services/spring-controllers/spring-controller-generator.js +3 -1
- package/esm/src/generators/services/spring-reactive-web-clients/spring-reactive-web-client-generator.js +1 -1
- package/package.json +1 -1
- package/script/src/generators/services/okhttp3-clients/okhttp3-client-generator.js +1 -1
- package/script/src/generators/services/spring-controllers/spring-controller-generator.d.ts.map +1 -1
- package/script/src/generators/services/spring-controllers/spring-controller-generator.js +3 -1
- package/script/src/generators/services/spring-reactive-web-clients/spring-reactive-web-client-generator.js +1 -1
- package/src/mod.ts +8 -0
- package/src/src/assets.ts +9 -0
- package/src/src/ast/_index.ts +66 -0
- package/src/src/ast/common.ts +1 -0
- package/src/src/ast/index.ts +1 -0
- package/src/src/ast/node.ts +10 -0
- package/src/src/ast/nodes/annotation.ts +79 -0
- package/src/src/ast/nodes/argument.ts +62 -0
- package/src/src/ast/nodes/call.ts +75 -0
- package/src/src/ast/nodes/class.ts +178 -0
- package/src/src/ast/nodes/collection-literal.ts +49 -0
- package/src/src/ast/nodes/constructor.ts +126 -0
- package/src/src/ast/nodes/doc-tag.ts +138 -0
- package/src/src/ast/nodes/doc.ts +111 -0
- package/src/src/ast/nodes/enum-value.ts +100 -0
- package/src/src/ast/nodes/enum.ts +163 -0
- package/src/src/ast/nodes/function.ts +178 -0
- package/src/src/ast/nodes/generic-parameter.ts +54 -0
- package/src/src/ast/nodes/init-block.ts +38 -0
- package/src/src/ast/nodes/interface.ts +133 -0
- package/src/src/ast/nodes/lambda-type.ts +73 -0
- package/src/src/ast/nodes/lambda.ts +74 -0
- package/src/src/ast/nodes/object.ts +102 -0
- package/src/src/ast/nodes/parameter.ts +118 -0
- package/src/src/ast/nodes/property.ts +225 -0
- package/src/src/ast/nodes/reference.ts +178 -0
- package/src/src/ast/nodes/string.ts +114 -0
- package/src/src/ast/nodes/types.ts +23 -0
- package/src/src/ast/references/index.ts +10 -0
- package/src/src/ast/references/jackson.ts +59 -0
- package/src/src/ast/references/jakarta.ts +14 -0
- package/src/src/ast/references/java.ts +27 -0
- package/src/src/ast/references/kotlin.ts +41 -0
- package/src/src/ast/references/kotlinx.ts +14 -0
- package/src/src/ast/references/okhttp3.ts +5 -0
- package/src/src/ast/references/reactor.ts +5 -0
- package/src/src/ast/references/spring-reactive.ts +37 -0
- package/src/src/ast/references/spring.ts +86 -0
- package/src/src/ast/references/swagger.ts +23 -0
- package/src/src/ast/utils/get-kotlin-builder-options.ts +19 -0
- package/src/src/ast/utils/to-kt-node.ts +31 -0
- package/src/src/ast/utils/write-kt-annotations.ts +15 -0
- package/src/src/ast/utils/write-kt-arguments.ts +45 -0
- package/src/src/ast/utils/write-kt-enum-values.ts +27 -0
- package/src/src/ast/utils/write-kt-generic-parameters.ts +12 -0
- package/src/src/ast/utils/write-kt-members.ts +25 -0
- package/src/src/ast/utils/write-kt-node.ts +37 -0
- package/src/src/ast/utils/write-kt-parameters.ts +25 -0
- package/src/src/common-results.ts +4 -0
- package/src/src/config.ts +61 -0
- package/src/src/file-builder.ts +112 -0
- package/src/src/generators/file-generator.ts +29 -0
- package/src/src/generators/index.ts +5 -0
- package/src/src/generators/models/args.ts +136 -0
- package/src/src/generators/models/index.ts +4 -0
- package/src/src/generators/models/model-generator.ts +736 -0
- package/src/src/generators/models/models-generator.ts +65 -0
- package/src/src/generators/models/models.ts +95 -0
- package/src/src/generators/services/okhttp3-clients/args.ts +90 -0
- package/src/src/generators/services/okhttp3-clients/index.ts +4 -0
- package/src/src/generators/services/okhttp3-clients/models.ts +73 -0
- package/src/src/generators/services/okhttp3-clients/okhttp3-client-generator.ts +627 -0
- package/src/src/generators/services/okhttp3-clients/okhttp3-clients-generator.ts +188 -0
- package/src/src/generators/services/okhttp3-clients/refs.ts +59 -0
- package/src/src/generators/services/spring-controllers/args.ts +95 -0
- package/src/src/generators/services/spring-controllers/index.ts +4 -0
- package/src/src/generators/services/spring-controllers/models.ts +76 -0
- package/src/src/generators/services/spring-controllers/refs.ts +17 -0
- package/src/src/generators/services/spring-controllers/spring-controller-generator.ts +1122 -0
- package/src/src/generators/services/spring-controllers/spring-controllers-generator.ts +140 -0
- package/src/src/generators/services/spring-reactive-web-clients/args.ts +112 -0
- package/src/src/generators/services/spring-reactive-web-clients/index.ts +4 -0
- package/src/src/generators/services/spring-reactive-web-clients/models.ts +78 -0
- package/src/src/generators/services/spring-reactive-web-clients/refs.ts +11 -0
- package/src/src/generators/services/spring-reactive-web-clients/spring-reactive-web-client-generator.ts +641 -0
- package/src/src/generators/services/spring-reactive-web-clients/spring-reactive-web-clients-generator.ts +125 -0
- package/src/src/import-collection.ts +98 -0
- package/src/src/types.ts +3 -0
- package/src/src/utils.ts +55 -0
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ApiEndpoint,
|
|
3
|
+
type ApiParameter,
|
|
4
|
+
type AppendValueGroup,
|
|
5
|
+
appendValueGroup,
|
|
6
|
+
type BasicAppendValue,
|
|
7
|
+
builderTemplate as s,
|
|
8
|
+
createOverwriteProxy,
|
|
9
|
+
getSourceDisplayName,
|
|
10
|
+
type MaybePromise,
|
|
11
|
+
notNullish,
|
|
12
|
+
resolveAnyOfAndAllOf,
|
|
13
|
+
SourceBuilder,
|
|
14
|
+
toCasing,
|
|
15
|
+
} from '@goast/core';
|
|
16
|
+
|
|
17
|
+
import { getReasonPhrase } from 'http-status-codes';
|
|
18
|
+
import { kt } from '../../../ast/index.js';
|
|
19
|
+
import type { KotlinImport } from '../../../common-results.js';
|
|
20
|
+
import { KotlinFileBuilder } from '../../../file-builder.js';
|
|
21
|
+
import type { ApiParameterWithMultipartInfo } from '../../../types.js';
|
|
22
|
+
import { getArrayItemStringEnumSchema, getSourceDocLine, getStringEnumSchema, modifyString } from '../../../utils.js';
|
|
23
|
+
import { KotlinFileGenerator } from '../../file-generator.js';
|
|
24
|
+
import type { DefaultKotlinSpringControllerGeneratorArgs as Args } from './index.js';
|
|
25
|
+
import type { KotlinServiceGeneratorContext, KotlinServiceGeneratorOutput } from './models.js';
|
|
26
|
+
|
|
27
|
+
type Context = KotlinServiceGeneratorContext;
|
|
28
|
+
type Output = KotlinServiceGeneratorOutput;
|
|
29
|
+
type Builder = KotlinFileBuilder;
|
|
30
|
+
|
|
31
|
+
const strictResponseCustomProperty = 'strict-response';
|
|
32
|
+
|
|
33
|
+
export interface KotlinSpringControllerGenerator<
|
|
34
|
+
TOutput extends Output = Output,
|
|
35
|
+
> {
|
|
36
|
+
generate(ctx: Context): MaybePromise<TOutput>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DefaultKotlinSpringControllerGenerator extends KotlinFileGenerator<Context, Output>
|
|
40
|
+
implements KotlinSpringControllerGenerator {
|
|
41
|
+
public generate(
|
|
42
|
+
ctx: KotlinServiceGeneratorContext,
|
|
43
|
+
): MaybePromise<KotlinServiceGeneratorOutput> {
|
|
44
|
+
const packageName = this.getPackageName(ctx, {});
|
|
45
|
+
const dirPath = this.getDirectoryPath(ctx, { packageName });
|
|
46
|
+
|
|
47
|
+
console.log(`Generating service ${ctx.service.id} to ${dirPath}...`);
|
|
48
|
+
console.log(` Endpoints:`);
|
|
49
|
+
ctx.service.endpoints.forEach((endpoint) => {
|
|
50
|
+
console.log(
|
|
51
|
+
` - ${getSourceDisplayName(ctx.data, endpoint)} [${toCasing(endpoint.name, ctx.config.functionNameCasing)}]`,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
apiInterface: this.generateApiInterfaceFile(ctx, {
|
|
56
|
+
dirPath,
|
|
57
|
+
packageName,
|
|
58
|
+
}),
|
|
59
|
+
apiController: this.generateApiControllerFile(ctx, {
|
|
60
|
+
dirPath,
|
|
61
|
+
packageName,
|
|
62
|
+
}),
|
|
63
|
+
apiDelegate: this.generateApiDelegateInterfaceFile(ctx, {
|
|
64
|
+
dirPath,
|
|
65
|
+
packageName,
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// #region API Interface
|
|
71
|
+
protected generateApiInterfaceFile(
|
|
72
|
+
ctx: Context,
|
|
73
|
+
args: Args.GenerateApiInterfaceFile,
|
|
74
|
+
): KotlinImport {
|
|
75
|
+
const { dirPath, packageName } = args;
|
|
76
|
+
const typeName = this.getApiInterfaceName(ctx, {});
|
|
77
|
+
const fileName = `${typeName}.kt`;
|
|
78
|
+
const filePath = `${dirPath}/${fileName}`;
|
|
79
|
+
console.log(` Generating API interface ${typeName} to ${fileName}...`);
|
|
80
|
+
|
|
81
|
+
const builder = new KotlinFileBuilder(packageName, ctx.config);
|
|
82
|
+
builder.append(
|
|
83
|
+
this.getApiInterfaceFileContent(ctx, { interfaceName: typeName }),
|
|
84
|
+
);
|
|
85
|
+
builder.writeToFile(filePath);
|
|
86
|
+
|
|
87
|
+
return { typeName, packageName };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
protected getApiInterfaceFileContent(
|
|
91
|
+
ctx: Context,
|
|
92
|
+
args: Args.GetApiInterfaceFileContent,
|
|
93
|
+
): AppendValueGroup<Builder> {
|
|
94
|
+
const { interfaceName } = args;
|
|
95
|
+
return appendValueGroup<Builder>(
|
|
96
|
+
[this.getApiInterface(ctx, { interfaceName })],
|
|
97
|
+
'\n',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected getApiInterface(
|
|
102
|
+
ctx: Context,
|
|
103
|
+
args: Args.GetApiInterface,
|
|
104
|
+
): kt.Interface<Builder> {
|
|
105
|
+
const { interfaceName } = args;
|
|
106
|
+
|
|
107
|
+
return kt.interface(interfaceName, {
|
|
108
|
+
annotations: this.getApiInterfaceAnnotations(ctx),
|
|
109
|
+
members: this.getApiInterfaceMembers(ctx),
|
|
110
|
+
companionObject: kt.object({
|
|
111
|
+
members: ctx.service.endpoints.map((endpoint) =>
|
|
112
|
+
kt.property(this.getPathConstantName(ctx, { endpoint }), {
|
|
113
|
+
const: true,
|
|
114
|
+
default: kt.string(this.getEndpointPath(ctx, { endpoint })),
|
|
115
|
+
})
|
|
116
|
+
),
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected getApiInterfaceAnnotations(ctx: Context): kt.Annotation<Builder>[] {
|
|
122
|
+
const validated = kt.annotation(kt.refs.spring.validated());
|
|
123
|
+
const requestMapping = kt.annotation(kt.refs.spring.requestMapping(), [
|
|
124
|
+
kt.argument(this.getControllerRequestMapping(ctx, { prefix: 'api' })),
|
|
125
|
+
]);
|
|
126
|
+
return [validated, requestMapping];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected getApiInterfaceMembers(ctx: Context): kt.InterfaceMember<Builder>[] {
|
|
130
|
+
const members: kt.InterfaceMember<Builder>[] = [];
|
|
131
|
+
const delegateInterfaceName = this.getApiDelegateInterfaceName(ctx, {});
|
|
132
|
+
|
|
133
|
+
members.push(
|
|
134
|
+
kt.function('getDelegate', {
|
|
135
|
+
returnType: delegateInterfaceName,
|
|
136
|
+
singleExpression: true,
|
|
137
|
+
body: kt.object({
|
|
138
|
+
implements: [delegateInterfaceName],
|
|
139
|
+
}),
|
|
140
|
+
}),
|
|
141
|
+
kt.function('getExceptionHandler', {
|
|
142
|
+
returnType: ctx.refs.apiExceptionHandler({ nullable: true }),
|
|
143
|
+
singleExpression: true,
|
|
144
|
+
body: kt.toNode(null),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
ctx.service.endpoints.forEach((endpoint) => {
|
|
149
|
+
members.push(this.getApiInterfaceEndpointMethod(ctx, { endpoint }));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
members.push(
|
|
153
|
+
...ctx.service.endpoints
|
|
154
|
+
.filter((endpoint) => endpoint.custom[strictResponseCustomProperty] ?? ctx.config.strictResponseEntities)
|
|
155
|
+
.map((endpoint) => this.getApiResponseEntityClass(ctx, { endpoint })),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return members;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected getApiInterfaceEndpointMethod(
|
|
162
|
+
ctx: Context,
|
|
163
|
+
args: Args.GetApiInterfaceEndpointMethod,
|
|
164
|
+
): kt.Function<Builder> {
|
|
165
|
+
const { endpoint } = args;
|
|
166
|
+
const parameters = this.getAllParameters(ctx, { endpoint });
|
|
167
|
+
|
|
168
|
+
return kt.function(toCasing(endpoint.name, ctx.config.functionNameCasing), {
|
|
169
|
+
doc: kt.doc(this.getEndpointDocDescription(ctx, { endpoint })),
|
|
170
|
+
suspend: ctx.config.suspendingFunctions,
|
|
171
|
+
annotations: this.getApiInterfaceEndpointMethodAnnnotations(
|
|
172
|
+
ctx,
|
|
173
|
+
endpoint,
|
|
174
|
+
),
|
|
175
|
+
parameters: parameters.map((parameter) => this.getApiInterfaceEndpointMethodParameter(ctx, endpoint, parameter)),
|
|
176
|
+
returnType: kt.refs.spring.responseEntity(['*']),
|
|
177
|
+
body: this.getApiInterfaceEndpointMethodBody(ctx, endpoint, parameters),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected getApiInterfaceEndpointMethodAnnnotations(
|
|
182
|
+
ctx: Context,
|
|
183
|
+
endpoint: ApiEndpoint,
|
|
184
|
+
): kt.Annotation<Builder>[] {
|
|
185
|
+
const annotations: kt.Annotation<Builder>[] = [];
|
|
186
|
+
|
|
187
|
+
if (ctx.config.addSwaggerAnnotations) {
|
|
188
|
+
annotations.push(
|
|
189
|
+
kt.annotation(kt.refs.swagger.operation(), [
|
|
190
|
+
endpoint.summary ? kt.argument.named('summary', kt.string(endpoint.summary?.trim())) : null,
|
|
191
|
+
kt.argument.named('operationId', kt.string(endpoint.name)),
|
|
192
|
+
endpoint.description
|
|
193
|
+
? kt.argument.named(
|
|
194
|
+
'description',
|
|
195
|
+
kt.string(endpoint.description?.trim()),
|
|
196
|
+
)
|
|
197
|
+
: null,
|
|
198
|
+
endpoint.deprecated !== undefined ? kt.argument.named('deprecated', kt.toNode(endpoint.deprecated)) : null,
|
|
199
|
+
]),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (endpoint.responses.length > 0) {
|
|
203
|
+
annotations.push(
|
|
204
|
+
kt.annotation(kt.refs.swagger.apiResponses(), [
|
|
205
|
+
kt.argument.named(
|
|
206
|
+
'value',
|
|
207
|
+
kt.collectionLiteral(
|
|
208
|
+
endpoint.responses.map((response) =>
|
|
209
|
+
kt.call(kt.refs.swagger.apiResponse(), [
|
|
210
|
+
kt.argument.named(
|
|
211
|
+
'responseCode',
|
|
212
|
+
kt.string(response.statusCode?.toString()),
|
|
213
|
+
),
|
|
214
|
+
response.description
|
|
215
|
+
? kt.argument.named(
|
|
216
|
+
'description',
|
|
217
|
+
kt.string(response.description?.trim()),
|
|
218
|
+
)
|
|
219
|
+
: null,
|
|
220
|
+
kt.argument.named(
|
|
221
|
+
'content',
|
|
222
|
+
kt.collectionLiteral(
|
|
223
|
+
(response.contentOptions.length === 0
|
|
224
|
+
? [{ schema: undefined, type: undefined }]
|
|
225
|
+
: response.contentOptions).map(
|
|
226
|
+
(content) => {
|
|
227
|
+
let schemaType: kt.Type<Builder> = this.getSchemaType(ctx, {
|
|
228
|
+
schema: content.schema,
|
|
229
|
+
}) ?? kt.refs.any();
|
|
230
|
+
let isArray = false;
|
|
231
|
+
|
|
232
|
+
if (kt.refs.list.matches(schemaType)) {
|
|
233
|
+
isArray = true;
|
|
234
|
+
schemaType = schemaType.generics[0];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let ktSchema = kt.call(kt.refs.swagger.schema(), [
|
|
238
|
+
kt.argument.named(
|
|
239
|
+
'implementation',
|
|
240
|
+
s<Builder>`${schemaType}::class`,
|
|
241
|
+
),
|
|
242
|
+
]);
|
|
243
|
+
if (isArray) {
|
|
244
|
+
ktSchema = kt.call(kt.refs.swagger.arraySchema(), [
|
|
245
|
+
kt.argument.named('schema', ktSchema),
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return kt.call(kt.refs.swagger.content(), [
|
|
250
|
+
content.type
|
|
251
|
+
? kt.argument.named(
|
|
252
|
+
'mediaType',
|
|
253
|
+
kt.string(content.type),
|
|
254
|
+
)
|
|
255
|
+
: null,
|
|
256
|
+
content.schema
|
|
257
|
+
? kt.argument.named(
|
|
258
|
+
isArray ? 'array' : 'schema',
|
|
259
|
+
ktSchema,
|
|
260
|
+
)
|
|
261
|
+
: null,
|
|
262
|
+
]);
|
|
263
|
+
},
|
|
264
|
+
),
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
])
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
]),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const requestMapping = kt.annotation(kt.refs.spring.requestMapping(), [
|
|
277
|
+
kt.argument.named(
|
|
278
|
+
'method',
|
|
279
|
+
kt.collectionLiteral([
|
|
280
|
+
kt.call([
|
|
281
|
+
kt.refs.spring.requestMethod(),
|
|
282
|
+
endpoint.method.toUpperCase(),
|
|
283
|
+
]),
|
|
284
|
+
]),
|
|
285
|
+
),
|
|
286
|
+
kt.argument.named(
|
|
287
|
+
'value',
|
|
288
|
+
kt.collectionLiteral([this.getPathConstantName(ctx, { endpoint })]),
|
|
289
|
+
),
|
|
290
|
+
]);
|
|
291
|
+
if (endpoint.requestBody && endpoint.requestBody.content.length > 0) {
|
|
292
|
+
requestMapping.arguments.push(
|
|
293
|
+
kt.argument.named(
|
|
294
|
+
'consumes',
|
|
295
|
+
kt.collectionLiteral(
|
|
296
|
+
endpoint.requestBody?.content.map((x) => kt.string(x.type)),
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
annotations.push(requestMapping);
|
|
302
|
+
|
|
303
|
+
return annotations;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected getApiInterfaceEndpointMethodParameter(
|
|
307
|
+
ctx: Context,
|
|
308
|
+
endpoint: ApiEndpoint,
|
|
309
|
+
parameter: ApiParameterWithMultipartInfo,
|
|
310
|
+
): kt.Parameter<Builder> {
|
|
311
|
+
const enumItemSchema = parameter.multipart ? undefined : getArrayItemStringEnumSchema(parameter.schema);
|
|
312
|
+
const enumSchema = parameter.multipart ? undefined : getStringEnumSchema(parameter.schema);
|
|
313
|
+
const enumValueSchema = enumSchema ?? enumItemSchema;
|
|
314
|
+
const actualType = this.getSchemaType(ctx, { schema: parameter.schema });
|
|
315
|
+
const isEnumSchema = !!enumValueSchema && !!actualType &&
|
|
316
|
+
!!this.getSchemaType(ctx, { schema: enumValueSchema });
|
|
317
|
+
const schemaType = !isEnumSchema
|
|
318
|
+
? actualType
|
|
319
|
+
: enumItemSchema
|
|
320
|
+
? kt.refs.list([kt.refs.string()], { nullable: actualType?.nullable })
|
|
321
|
+
: kt.refs.string({ nullable: actualType?.nullable });
|
|
322
|
+
const result = kt.parameter(
|
|
323
|
+
toCasing(parameter.name, ctx.config.parameterNameCasing),
|
|
324
|
+
this.getParameterType(ctx, {
|
|
325
|
+
endpoint,
|
|
326
|
+
parameter,
|
|
327
|
+
type: isEnumSchema ? schemaType : undefined,
|
|
328
|
+
}),
|
|
329
|
+
{
|
|
330
|
+
default: parameter.multipart && parameter.schema?.default !== undefined
|
|
331
|
+
? kt.toNode(parameter.schema?.default)
|
|
332
|
+
: null,
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (ctx.config.addSwaggerAnnotations) {
|
|
337
|
+
const annotation = kt.annotation(kt.refs.swagger.parameter(), [
|
|
338
|
+
parameter.multipart ? kt.argument.named('name', kt.string(parameter.multipart.name)) : null,
|
|
339
|
+
parameter.description
|
|
340
|
+
? kt.argument.named(
|
|
341
|
+
'description',
|
|
342
|
+
kt.string(parameter.description?.trim()),
|
|
343
|
+
)
|
|
344
|
+
: null,
|
|
345
|
+
kt.argument.named('required', parameter.required),
|
|
346
|
+
parameter.target === 'header' ? kt.argument.named('hidden', kt.toNode(true)) : null,
|
|
347
|
+
]);
|
|
348
|
+
|
|
349
|
+
const schemaArgs: kt.Argument<SourceBuilder>[] = [];
|
|
350
|
+
if (parameter.schema?.default !== undefined) {
|
|
351
|
+
schemaArgs.push(
|
|
352
|
+
kt.argument.named(
|
|
353
|
+
'defaultValue',
|
|
354
|
+
kt.string(String(parameter.schema?.default)),
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (isEnumSchema) {
|
|
359
|
+
schemaArgs.push(
|
|
360
|
+
kt.argument.named(
|
|
361
|
+
'allowableValues',
|
|
362
|
+
kt.collectionLiteral(
|
|
363
|
+
enumValueSchema?.enum?.map((x) => kt.string(x?.toString())),
|
|
364
|
+
),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
if (schemaArgs.length) {
|
|
369
|
+
annotation.arguments.push(
|
|
370
|
+
kt.argument.named(
|
|
371
|
+
'schema',
|
|
372
|
+
kt.call([kt.refs.swagger.schema()], schemaArgs),
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
result.annotations.push(annotation);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const isCorePackage = !schemaType?.packageName ||
|
|
380
|
+
/^(kotlin|java)(\..*|$)/.test(schemaType.packageName);
|
|
381
|
+
if (!isCorePackage && ctx.config.addJakartaValidationAnnotations) {
|
|
382
|
+
result.annotations.push(kt.annotation(kt.refs.jakarta.valid()));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (parameter.target === 'body' && !parameter.multipart) {
|
|
386
|
+
// A bare @RequestBody is required as far as Spring is concerned, so an optional body has to say so in the
|
|
387
|
+
// annotation too - the nullable parameter type alone still answers 400 when the body is missing.
|
|
388
|
+
result.annotations.push(
|
|
389
|
+
kt.annotation(
|
|
390
|
+
kt.refs.spring.requestBody(),
|
|
391
|
+
parameter.required ? [] : [kt.argument.named('required', parameter.required)],
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (parameter.target === 'query') {
|
|
397
|
+
const annotation = kt.annotation(kt.refs.spring.requestParam(), [
|
|
398
|
+
kt.argument.named('value', kt.string(parameter.name)),
|
|
399
|
+
kt.argument.named('required', parameter.required),
|
|
400
|
+
]);
|
|
401
|
+
if (parameter.schema?.default !== undefined) {
|
|
402
|
+
annotation.arguments.push(
|
|
403
|
+
kt.argument.named(
|
|
404
|
+
'defaultValue',
|
|
405
|
+
kt.string(String(parameter.schema?.default)),
|
|
406
|
+
),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
result.annotations.push(annotation);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (parameter.target === 'path') {
|
|
413
|
+
result.annotations.push(
|
|
414
|
+
kt.annotation(kt.refs.spring.pathVariable(), [
|
|
415
|
+
kt.string(parameter.name),
|
|
416
|
+
]),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (parameter.target === 'header') {
|
|
421
|
+
result.annotations.push(
|
|
422
|
+
kt.annotation(kt.refs.spring.requestHeader(), [
|
|
423
|
+
kt.string(parameter.name),
|
|
424
|
+
]),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (parameter.multipart) {
|
|
429
|
+
result.annotations.push(
|
|
430
|
+
kt.annotation(kt.refs.spring.requestPart(), [
|
|
431
|
+
kt.argument.named('value', kt.string(parameter.multipart.name)),
|
|
432
|
+
kt.argument.named('required', parameter.required),
|
|
433
|
+
]),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
protected getApiInterfaceEndpointMethodBody(
|
|
441
|
+
ctx: Context,
|
|
442
|
+
endpoint: ApiEndpoint,
|
|
443
|
+
parameters: ApiParameterWithMultipartInfo[],
|
|
444
|
+
): AppendValueGroup<Builder> {
|
|
445
|
+
const body = appendValueGroup<Builder>([], '\n');
|
|
446
|
+
|
|
447
|
+
parameters.forEach((x) => {
|
|
448
|
+
const paramName = toCasing(x.name, ctx.config.parameterNameCasing);
|
|
449
|
+
if (x.multipart) return;
|
|
450
|
+
|
|
451
|
+
const enumItemSchema = getArrayItemStringEnumSchema(x.schema);
|
|
452
|
+
const enumValueSchema = getStringEnumSchema(x.schema) ?? enumItemSchema;
|
|
453
|
+
if (!enumValueSchema) return;
|
|
454
|
+
|
|
455
|
+
const schemaType = this.getSchemaType(ctx, { schema: x.schema });
|
|
456
|
+
const enumType = this.getSchemaType(ctx, { schema: enumValueSchema });
|
|
457
|
+
if (schemaType && enumType) {
|
|
458
|
+
body.values.push(
|
|
459
|
+
s`val ${paramName} = ${paramName}${schemaType.nullable || (!x.required && !x.schema?.default) ? '?' : ''}.${
|
|
460
|
+
enumItemSchema ? 'map' : 'let'
|
|
461
|
+
} { ${enumType}.fromValue(it) ?: return ${kt.refs.spring.responseEntity.infer()}.status(${kt.refs.spring.httpStatus()}.BAD_REQUEST).body(${
|
|
462
|
+
kt.string(
|
|
463
|
+
`Invalid value for parameter ${x.name}`,
|
|
464
|
+
)
|
|
465
|
+
}) }`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
body.values.push(
|
|
471
|
+
s`try {${s.indent`
|
|
472
|
+
return ${
|
|
473
|
+
kt.call(
|
|
474
|
+
[
|
|
475
|
+
kt.call(kt.reference('getDelegate'), []),
|
|
476
|
+
toCasing(endpoint.name, ctx.config.functionNameCasing),
|
|
477
|
+
],
|
|
478
|
+
parameters.map((x) => toCasing(x.name, ctx.config.parameterNameCasing)),
|
|
479
|
+
)
|
|
480
|
+
}`}
|
|
481
|
+
} catch (e: ${kt.refs.throwable()}) {${s.indent`
|
|
482
|
+
return getExceptionHandler()?.handleApiException(e) ?: throw e`}
|
|
483
|
+
}`,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return body;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
protected getApiResponseEntityClass(
|
|
490
|
+
ctx: Context,
|
|
491
|
+
args: Args.GetApiResponseEntityClass,
|
|
492
|
+
): kt.Class<Builder> {
|
|
493
|
+
const { endpoint } = args;
|
|
494
|
+
const name = this.getApiResponseEntityName(ctx, { endpoint });
|
|
495
|
+
// Spring 7's `ResponseEntity<T>` carries an `Any` upper bound (JSpecify), so the subclass must bound `T`.
|
|
496
|
+
// The body parameter is then made nullable (`T?`) to keep null-body ("no response body") responses legal
|
|
497
|
+
// while staying wire-identical, since Spring's super constructor accepts a `@Nullable T body`.
|
|
498
|
+
const springBoot4 = ctx.config.springBootVersion === 4;
|
|
499
|
+
return kt.class(name, {
|
|
500
|
+
doc: kt.doc(`Response entity for ${endpoint.name}.`),
|
|
501
|
+
generics: [kt.genericParameter('T', springBoot4 ? { constraint: kt.refs.any() } : undefined)],
|
|
502
|
+
primaryConstructor: kt.constructor(
|
|
503
|
+
[
|
|
504
|
+
kt.parameter.class('body', kt.reference('T', null, springBoot4 ? { nullable: true } : undefined)),
|
|
505
|
+
kt.parameter.class('rawStatus', kt.refs.int()),
|
|
506
|
+
kt.parameter.class(
|
|
507
|
+
'headers',
|
|
508
|
+
kt.refs.spring.multiValueMap([kt.refs.string(), kt.refs.string()], {
|
|
509
|
+
nullable: true,
|
|
510
|
+
}),
|
|
511
|
+
{ default: kt.toNode(null) },
|
|
512
|
+
),
|
|
513
|
+
],
|
|
514
|
+
null,
|
|
515
|
+
{
|
|
516
|
+
accessModifier: endpoint.responses.length > 0 ? 'private' : null,
|
|
517
|
+
delegateTarget: 'super',
|
|
518
|
+
delegateArguments: [
|
|
519
|
+
kt.argument('body'),
|
|
520
|
+
kt.argument('headers'),
|
|
521
|
+
kt.argument('rawStatus'),
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
),
|
|
525
|
+
extends: kt.refs.spring.responseEntity([kt.reference('T')]),
|
|
526
|
+
companionObject: kt.object({
|
|
527
|
+
members: Array.from(
|
|
528
|
+
new Set(
|
|
529
|
+
[
|
|
530
|
+
...ctx.config.defaultStatusCodes,
|
|
531
|
+
501,
|
|
532
|
+
...endpoint.responses.map((x) => x.statusCode),
|
|
533
|
+
].filter(notNullish),
|
|
534
|
+
),
|
|
535
|
+
).map((code) => {
|
|
536
|
+
const fnName = toCasing(
|
|
537
|
+
getReasonPhrase(code),
|
|
538
|
+
ctx.config.functionNameCasing,
|
|
539
|
+
);
|
|
540
|
+
const response = endpoint.responses.find(
|
|
541
|
+
(x) => x.statusCode === code,
|
|
542
|
+
);
|
|
543
|
+
const responseType = response ? this.getResponseType(ctx, { endpoint, response }) : undefined;
|
|
544
|
+
const contentType = response?.contentOptions.find(
|
|
545
|
+
(x) => x.schema,
|
|
546
|
+
)?.type;
|
|
547
|
+
|
|
548
|
+
const hasResponseBody = responseType && !kt.refs.unit.matches(responseType);
|
|
549
|
+
return kt.function(fnName, {
|
|
550
|
+
parameters: [
|
|
551
|
+
hasResponseBody ? kt.parameter.class('body', responseType) : null,
|
|
552
|
+
kt.parameter.class(
|
|
553
|
+
'headers',
|
|
554
|
+
kt.refs.spring.multiValueMap(
|
|
555
|
+
[kt.refs.string(), kt.refs.string()],
|
|
556
|
+
{ nullable: true },
|
|
557
|
+
),
|
|
558
|
+
{ default: kt.toNode(null) },
|
|
559
|
+
),
|
|
560
|
+
],
|
|
561
|
+
singleExpression: true,
|
|
562
|
+
body: kt.call(
|
|
563
|
+
[
|
|
564
|
+
kt.reference(
|
|
565
|
+
this.getApiResponseEntityName(ctx, { endpoint }),
|
|
566
|
+
null,
|
|
567
|
+
{
|
|
568
|
+
generics: [
|
|
569
|
+
hasResponseBody
|
|
570
|
+
? responseType
|
|
571
|
+
// `Unit?` violates the `T : Any` bound under Spring Boot 4; the `null` body argument
|
|
572
|
+
// below stays (now legal because the subclass body parameter is nullable).
|
|
573
|
+
: (springBoot4 ? kt.refs.unit() : kt.refs.unit({ nullable: true })),
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
),
|
|
577
|
+
],
|
|
578
|
+
[
|
|
579
|
+
hasResponseBody ? kt.argument('body') : kt.toNode(null),
|
|
580
|
+
kt.toNode(code),
|
|
581
|
+
contentType
|
|
582
|
+
? s`${kt.refs.spring.linkedMultiValueMap([kt.refs.string(), kt.refs.string()])}().also {
|
|
583
|
+
if (headers != null) {
|
|
584
|
+
it.putAll(headers)
|
|
585
|
+
}
|
|
586
|
+
it.addIfAbsent("Content-Type", ${kt.string(contentType)})
|
|
587
|
+
}`
|
|
588
|
+
: kt.argument('headers'),
|
|
589
|
+
],
|
|
590
|
+
),
|
|
591
|
+
});
|
|
592
|
+
}),
|
|
593
|
+
}),
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// #endregion
|
|
597
|
+
|
|
598
|
+
// #region API Controller
|
|
599
|
+
protected generateApiControllerFile(
|
|
600
|
+
ctx: Context,
|
|
601
|
+
args: Args.GenerateApiControllerFile,
|
|
602
|
+
): KotlinImport {
|
|
603
|
+
const { dirPath, packageName } = args;
|
|
604
|
+
const typeName = this.getApiControllerName(ctx, {});
|
|
605
|
+
const fileName = `${typeName}.kt`;
|
|
606
|
+
const filePath = `${dirPath}/${fileName}`;
|
|
607
|
+
console.log(` Generating API controller ${typeName} to ${fileName}...`);
|
|
608
|
+
|
|
609
|
+
const builder = new KotlinFileBuilder(packageName, ctx.config);
|
|
610
|
+
builder.append(
|
|
611
|
+
this.getApiControllerFileContent(ctx, { controllerName: typeName }),
|
|
612
|
+
);
|
|
613
|
+
builder.writeToFile(filePath);
|
|
614
|
+
|
|
615
|
+
return { typeName, packageName };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
protected getApiControllerFileContent(
|
|
619
|
+
ctx: Context,
|
|
620
|
+
args: Args.GetApiControllerFileContent,
|
|
621
|
+
): AppendValueGroup<Builder> {
|
|
622
|
+
const { controllerName } = args;
|
|
623
|
+
|
|
624
|
+
return appendValueGroup(
|
|
625
|
+
[this.getApiController(ctx, { controllerName })],
|
|
626
|
+
'\n',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
protected getApiController(
|
|
631
|
+
ctx: Context,
|
|
632
|
+
args: Args.GetApiController,
|
|
633
|
+
): kt.Class<Builder> {
|
|
634
|
+
const { controllerName } = args;
|
|
635
|
+
|
|
636
|
+
return kt.class(controllerName, {
|
|
637
|
+
annotations: this.getApiControllerAnnotations(ctx),
|
|
638
|
+
primaryConstructor: kt.constructor([
|
|
639
|
+
kt.parameter.class(
|
|
640
|
+
'delegate',
|
|
641
|
+
kt.reference(this.getApiDelegateInterfaceName(ctx, {}), null, {
|
|
642
|
+
nullable: true,
|
|
643
|
+
}),
|
|
644
|
+
{
|
|
645
|
+
annotations: [
|
|
646
|
+
kt.annotation(kt.refs.spring.autowired(), [
|
|
647
|
+
kt.argument.named('required', 'false'),
|
|
648
|
+
]),
|
|
649
|
+
],
|
|
650
|
+
},
|
|
651
|
+
),
|
|
652
|
+
kt.parameter.class(
|
|
653
|
+
'exceptionHandler',
|
|
654
|
+
ctx.refs.apiExceptionHandler({ nullable: true }),
|
|
655
|
+
{
|
|
656
|
+
annotations: [
|
|
657
|
+
kt.annotation(kt.refs.spring.autowired(), [
|
|
658
|
+
kt.argument.named('required', 'false'),
|
|
659
|
+
]),
|
|
660
|
+
],
|
|
661
|
+
accessModifier: 'private',
|
|
662
|
+
property: 'readonly',
|
|
663
|
+
},
|
|
664
|
+
),
|
|
665
|
+
]),
|
|
666
|
+
implements: [this.getApiInterfaceName(ctx, {})],
|
|
667
|
+
members: this.getApiControllerMembers(ctx),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
protected getApiControllerAnnotations(ctx: Context): kt.Annotation<Builder>[] {
|
|
672
|
+
const annotations: kt.Annotation<Builder>[] = [];
|
|
673
|
+
if (ctx.config.addJakartaValidationAnnotations) {
|
|
674
|
+
annotations.push(
|
|
675
|
+
kt.annotation(kt.refs.jakarta.generated(), [
|
|
676
|
+
kt.argument.named(
|
|
677
|
+
'value',
|
|
678
|
+
kt.collectionLiteral([
|
|
679
|
+
kt.string('com.goast.kotlin.spring-service-generator'),
|
|
680
|
+
]),
|
|
681
|
+
),
|
|
682
|
+
]),
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
annotations.push(kt.annotation(kt.refs.spring.controller()));
|
|
686
|
+
annotations.push(
|
|
687
|
+
kt.annotation(kt.refs.spring.requestMapping(), [
|
|
688
|
+
this.getControllerRequestMapping(ctx, {}),
|
|
689
|
+
]),
|
|
690
|
+
);
|
|
691
|
+
return annotations;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
protected getApiControllerMembers(ctx: Context): kt.ClassMember<Builder>[] {
|
|
695
|
+
const delegateInterfaceName = this.getApiDelegateInterfaceName(ctx, {});
|
|
696
|
+
|
|
697
|
+
const delegateProp = kt.property<Builder>('delegate', {
|
|
698
|
+
accessModifier: 'private',
|
|
699
|
+
default: s`delegate ?: ${kt.object({ implements: [delegateInterfaceName] })}`,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const getDelegateFun = kt.function<Builder>('getDelegate', {
|
|
703
|
+
override: true,
|
|
704
|
+
returnType: delegateInterfaceName,
|
|
705
|
+
singleExpression: true,
|
|
706
|
+
body: kt.reference('delegate'),
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const getExceptionHandlerFun = kt.function<Builder>('getExceptionHandler', {
|
|
710
|
+
override: true,
|
|
711
|
+
returnType: ctx.refs.apiExceptionHandler({ nullable: true }),
|
|
712
|
+
singleExpression: true,
|
|
713
|
+
body: kt.reference('exceptionHandler'),
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
return [delegateProp, getDelegateFun, getExceptionHandlerFun];
|
|
717
|
+
}
|
|
718
|
+
// #endregion
|
|
719
|
+
|
|
720
|
+
// #region API Delegate Interface
|
|
721
|
+
protected generateApiDelegateInterfaceFile(
|
|
722
|
+
ctx: Context,
|
|
723
|
+
args: Args.GenerateApiDelegateInterfaceFile,
|
|
724
|
+
): KotlinImport {
|
|
725
|
+
const { dirPath, packageName } = args;
|
|
726
|
+
const typeName = this.getApiDelegateInterfaceName(ctx, {});
|
|
727
|
+
const fileName = `${typeName}.kt`;
|
|
728
|
+
const filePath = `${dirPath}/${fileName}`;
|
|
729
|
+
console.log(` Generating API delegate ${typeName} to ${fileName}...`);
|
|
730
|
+
|
|
731
|
+
const builder = new KotlinFileBuilder(packageName, ctx.config);
|
|
732
|
+
builder.append(
|
|
733
|
+
this.getApiDelegateInterfaceFileContent(ctx, {
|
|
734
|
+
delegateInterfaceName: typeName,
|
|
735
|
+
}),
|
|
736
|
+
);
|
|
737
|
+
builder.writeToFile(filePath);
|
|
738
|
+
|
|
739
|
+
return { typeName, packageName };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
protected getApiDelegateInterfaceFileContent(
|
|
743
|
+
ctx: Context,
|
|
744
|
+
args: Args.GetApiDelegateInterfaceFileContent,
|
|
745
|
+
): AppendValueGroup<Builder> {
|
|
746
|
+
const { delegateInterfaceName } = args;
|
|
747
|
+
|
|
748
|
+
return appendValueGroup(
|
|
749
|
+
[this.getApiDelegateInterface(ctx, { delegateInterfaceName })],
|
|
750
|
+
'\n',
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
protected getApiDelegateInterface(
|
|
755
|
+
ctx: Context,
|
|
756
|
+
args: Args.GetApiDelegateInterface,
|
|
757
|
+
): kt.Interface<Builder> {
|
|
758
|
+
const { delegateInterfaceName } = args;
|
|
759
|
+
|
|
760
|
+
return kt.interface(delegateInterfaceName, {
|
|
761
|
+
annotations: this.getApiDelegateInterfaceAnnotations(ctx),
|
|
762
|
+
members: this.getApiDelegateInterfaceMembers(ctx),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
protected getApiDelegateInterfaceAnnotations(
|
|
767
|
+
ctx: Context,
|
|
768
|
+
): kt.Annotation<Builder>[] {
|
|
769
|
+
const annotations: kt.Annotation<Builder>[] = [];
|
|
770
|
+
if (ctx.config.addJakartaValidationAnnotations) {
|
|
771
|
+
annotations.push(
|
|
772
|
+
kt.annotation(kt.refs.jakarta.generated(), [
|
|
773
|
+
kt.argument.named(
|
|
774
|
+
'value',
|
|
775
|
+
kt.collectionLiteral([
|
|
776
|
+
kt.string('com.goast.kotlin.spring-service-generator'),
|
|
777
|
+
]),
|
|
778
|
+
),
|
|
779
|
+
]),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return annotations;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
protected getApiDelegateInterfaceMembers(
|
|
786
|
+
ctx: Context,
|
|
787
|
+
): kt.InterfaceMember<Builder>[] {
|
|
788
|
+
const members: kt.InterfaceMember<Builder>[] = [];
|
|
789
|
+
|
|
790
|
+
members.push(
|
|
791
|
+
kt.function('getRequest', {
|
|
792
|
+
returnType: kt.refs.java.optional([kt.refs.spring.nativeWebRequest()]),
|
|
793
|
+
singleExpression: true,
|
|
794
|
+
body: kt.call([kt.refs.java.optional.infer(), 'empty'], []),
|
|
795
|
+
}),
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
ctx.service.endpoints.forEach((endpoint) => {
|
|
799
|
+
members.push(
|
|
800
|
+
this.getApiDelegateInterfaceEndpointMethod(ctx, { endpoint }),
|
|
801
|
+
);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
return members;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
protected getApiDelegateInterfaceEndpointMethod(
|
|
808
|
+
ctx: Context,
|
|
809
|
+
args: Args.GetApiDelegateInterfaceEndpointMethod,
|
|
810
|
+
): kt.Function<Builder> {
|
|
811
|
+
const { endpoint } = args;
|
|
812
|
+
const parameters = this.getAllParameters(ctx, { endpoint });
|
|
813
|
+
|
|
814
|
+
const fn = kt.function<Builder>(
|
|
815
|
+
toCasing(endpoint.name, ctx.config.functionNameCasing),
|
|
816
|
+
{
|
|
817
|
+
doc: kt.doc(this.getEndpointDocDescription(ctx, { endpoint })),
|
|
818
|
+
suspend: ctx.config.suspendingFunctions,
|
|
819
|
+
parameters: parameters.map((parameter) => {
|
|
820
|
+
return kt.parameter(
|
|
821
|
+
toCasing(parameter.name, ctx.config.parameterNameCasing),
|
|
822
|
+
this.getParameterType(ctx, { endpoint, parameter }),
|
|
823
|
+
);
|
|
824
|
+
}),
|
|
825
|
+
},
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
if (endpoint.custom[strictResponseCustomProperty] ?? ctx.config.strictResponseEntities) {
|
|
829
|
+
const responseEntity = kt.reference.genericFactory(
|
|
830
|
+
this.getApiResponseEntityName(ctx, { endpoint }),
|
|
831
|
+
`${this.getPackageName(ctx, {})}.${this.getApiInterfaceName(ctx, {})}`,
|
|
832
|
+
);
|
|
833
|
+
fn.returnType = responseEntity(['*']);
|
|
834
|
+
fn.body = appendValueGroup(
|
|
835
|
+
[
|
|
836
|
+
s`return ${
|
|
837
|
+
kt.call(
|
|
838
|
+
[
|
|
839
|
+
responseEntity.infer(),
|
|
840
|
+
toCasing(getReasonPhrase(501), ctx.config.functionNameCasing),
|
|
841
|
+
],
|
|
842
|
+
[],
|
|
843
|
+
)
|
|
844
|
+
}`,
|
|
845
|
+
],
|
|
846
|
+
'\n',
|
|
847
|
+
);
|
|
848
|
+
} else {
|
|
849
|
+
fn.returnType = kt.refs.spring.responseEntity([
|
|
850
|
+
this.getResponseType(ctx, { endpoint }),
|
|
851
|
+
]);
|
|
852
|
+
fn.body = appendValueGroup(
|
|
853
|
+
[
|
|
854
|
+
s`return ${kt.refs.spring.responseEntity.infer()}(${kt.refs.spring.httpStatus()}.NOT_IMPLEMENTED)`,
|
|
855
|
+
],
|
|
856
|
+
'\n',
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return fn;
|
|
861
|
+
}
|
|
862
|
+
// #endregion
|
|
863
|
+
|
|
864
|
+
protected getParameterType(
|
|
865
|
+
ctx: Context,
|
|
866
|
+
args: Args.GetParameterType,
|
|
867
|
+
): kt.Type<Builder> {
|
|
868
|
+
const { parameter } = args;
|
|
869
|
+
if (parameter.multipart?.isFile) {
|
|
870
|
+
return kt.refs.spring.filePart();
|
|
871
|
+
}
|
|
872
|
+
const type = this.getTypeUsage(ctx, {
|
|
873
|
+
schema: parameter.schema,
|
|
874
|
+
nullable: (!parameter.required && parameter.schema?.default === undefined) ||
|
|
875
|
+
undefined,
|
|
876
|
+
type: args.type,
|
|
877
|
+
});
|
|
878
|
+
return parameter.target === 'body' ? adjustListType(ctx, type) : type;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
protected getResponseType(
|
|
882
|
+
ctx: Context,
|
|
883
|
+
args: Args.GetResponseType,
|
|
884
|
+
): kt.Type<Builder> {
|
|
885
|
+
const { endpoint, response } = args;
|
|
886
|
+
const responseSchemas = (response ? [response] : endpoint.responses)
|
|
887
|
+
.flatMap((x) => x.contentOptions.flatMap((x) => x.schema))
|
|
888
|
+
.filter(notNullish)
|
|
889
|
+
.filter(
|
|
890
|
+
(x, i, a) =>
|
|
891
|
+
a.findIndex((y) => {
|
|
892
|
+
const xType = this.getSchemaType(ctx, { schema: x });
|
|
893
|
+
const yType = this.getSchemaType(ctx, { schema: y });
|
|
894
|
+
return (
|
|
895
|
+
xType?.name === yType?.name &&
|
|
896
|
+
xType?.packageName === yType?.packageName
|
|
897
|
+
);
|
|
898
|
+
}) === i,
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
if (responseSchemas.length === 1) {
|
|
902
|
+
return adjustListType(
|
|
903
|
+
ctx,
|
|
904
|
+
this.getTypeUsage(ctx, {
|
|
905
|
+
schema: responseSchemas[0],
|
|
906
|
+
fallback: kt.refs.unit(),
|
|
907
|
+
}),
|
|
908
|
+
);
|
|
909
|
+
} else if (responseSchemas.length === 0) {
|
|
910
|
+
return kt.refs.unit();
|
|
911
|
+
} else {
|
|
912
|
+
// Under Spring Boot 4, this type flows into `ResponseEntity<T : Any>` (e.g. the delegate default methods),
|
|
913
|
+
// so the ambiguous/unknown-body fallback must be non-null `Any` rather than `Any?`.
|
|
914
|
+
return ctx.config.springBootVersion === 4 ? kt.refs.any() : kt.refs.any({ nullable: true });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
protected getTypeUsage(
|
|
919
|
+
ctx: Context,
|
|
920
|
+
args: Args.GetTypeUsage<Builder>,
|
|
921
|
+
): kt.Type<Builder> {
|
|
922
|
+
const { schema, nullable, fallback } = args;
|
|
923
|
+
const type = args.type ?? this.getSchemaType(ctx, { schema });
|
|
924
|
+
return type
|
|
925
|
+
? createOverwriteProxy(type, { nullable: nullable ?? type.nullable })
|
|
926
|
+
: (fallback ?? kt.refs.any({ nullable }));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
protected getSchemaType(
|
|
930
|
+
ctx: Context,
|
|
931
|
+
args: Args.GetSchemaType,
|
|
932
|
+
): kt.Reference<SourceBuilder> | undefined {
|
|
933
|
+
const { schema } = args;
|
|
934
|
+
return schema && ctx.input.kotlin.models[schema.id].type;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
protected getEndpointDocDescription(ctx: Context, args: Args.GetEndpointDocDescription): BasicAppendValue<Builder> {
|
|
938
|
+
const docSegments = [args.endpoint.description?.trim()];
|
|
939
|
+
|
|
940
|
+
if (ctx.config.includeSourceInDocs) {
|
|
941
|
+
docSegments.push(getSourceDocLine(args.endpoint));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return docSegments.filter(notNullish).join('\n\n');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
protected getControllerRequestMapping(
|
|
948
|
+
ctx: Context,
|
|
949
|
+
args: Args.GetControllerRequestMapping,
|
|
950
|
+
): kt.String<Builder> {
|
|
951
|
+
let { prefix } = args;
|
|
952
|
+
const basePath = this.getBasePath(ctx, {});
|
|
953
|
+
prefix ??= `openapi.${toCasing(ctx.service.name, ctx.config.propertyNameCasing)}`;
|
|
954
|
+
return kt.string(`\${${prefix}.base-path:${basePath}}`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
protected getBasePath(ctx: Context, _args: Args.GetBasePath): string {
|
|
958
|
+
return modifyString(
|
|
959
|
+
(ctx.service.$src ?? ctx.service.endpoints[0]?.$src)?.document
|
|
960
|
+
.servers?.[0]?.url ?? '/',
|
|
961
|
+
ctx.config.basePath,
|
|
962
|
+
ctx.service,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
protected getEndpointPath(ctx: Context, args: Args.GetEndpointPath): string {
|
|
967
|
+
const { endpoint } = args;
|
|
968
|
+
return modifyString(endpoint.path, ctx.config.pathModifier, endpoint);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
protected getDirectoryPath(
|
|
972
|
+
ctx: Context,
|
|
973
|
+
args: Args.GetDirectoryPath,
|
|
974
|
+
): string {
|
|
975
|
+
const { packageName } = args;
|
|
976
|
+
return `${ctx.config.outputDir}/${packageName.replace(/\./g, '/')}`;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
protected getPathConstantName(
|
|
980
|
+
ctx: Context,
|
|
981
|
+
args: Args.GetPathConstantName,
|
|
982
|
+
): string {
|
|
983
|
+
const { endpoint } = args;
|
|
984
|
+
return toCasing(`${endpoint.name}_path`, ctx.config.constantNameCasing);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
protected getPackageName(ctx: Context, _args: Args.GetPackageName): string {
|
|
988
|
+
const packageSuffix = typeof ctx.config.packageSuffix === 'string'
|
|
989
|
+
? ctx.config.packageSuffix
|
|
990
|
+
: ctx.config.packageSuffix(ctx.service);
|
|
991
|
+
return ctx.config.packageName + packageSuffix;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
protected getApiResponseEntityName(
|
|
995
|
+
ctx: Context,
|
|
996
|
+
args: Args.GetApiResponseEntityName,
|
|
997
|
+
): string {
|
|
998
|
+
const { endpoint } = args;
|
|
999
|
+
return toCasing(
|
|
1000
|
+
`${endpoint.name}_ResponseEntity`,
|
|
1001
|
+
ctx.config.typeNameCasing,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
protected getApiInterfaceName(
|
|
1006
|
+
ctx: Context,
|
|
1007
|
+
_args: Args.GetApiInterfaceName,
|
|
1008
|
+
): string {
|
|
1009
|
+
return toCasing(ctx.service.name + '_Api', ctx.config.typeNameCasing);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
protected getApiControllerName(
|
|
1013
|
+
ctx: Context,
|
|
1014
|
+
_args: Args.GetApiControllerName,
|
|
1015
|
+
): string {
|
|
1016
|
+
return toCasing(
|
|
1017
|
+
ctx.service.name + '_ApiController',
|
|
1018
|
+
ctx.config.typeNameCasing,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
protected getApiDelegateInterfaceName(
|
|
1023
|
+
ctx: Context,
|
|
1024
|
+
_args: Args.GetApiDelegateInterfaceName,
|
|
1025
|
+
): string {
|
|
1026
|
+
return toCasing(
|
|
1027
|
+
ctx.service.name + '_ApiDelegate',
|
|
1028
|
+
ctx.config.typeNameCasing,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
protected getAllParameters(
|
|
1033
|
+
ctx: Context,
|
|
1034
|
+
args: Args.GetAllParameters,
|
|
1035
|
+
): ApiParameterWithMultipartInfo[] {
|
|
1036
|
+
const { endpoint } = args;
|
|
1037
|
+
const parameters = endpoint.parameters.filter(
|
|
1038
|
+
(parameter) =>
|
|
1039
|
+
parameter.target === 'query' ||
|
|
1040
|
+
parameter.target === 'path' ||
|
|
1041
|
+
parameter.target === 'header',
|
|
1042
|
+
);
|
|
1043
|
+
if (endpoint.requestBody) {
|
|
1044
|
+
const content = endpoint.requestBody.content[0];
|
|
1045
|
+
let schema = content.schema;
|
|
1046
|
+
|
|
1047
|
+
if (content.type === 'multipart/form-data') {
|
|
1048
|
+
if (schema && schema.kind === 'object') {
|
|
1049
|
+
schema = resolveAnyOfAndAllOf(schema, true) ?? schema;
|
|
1050
|
+
const properties = schema.properties ?? {};
|
|
1051
|
+
for (const [name, property] of properties.entries()) {
|
|
1052
|
+
parameters.push(
|
|
1053
|
+
Object.assign(
|
|
1054
|
+
this.createApiParameter({
|
|
1055
|
+
id: `multipart-${name}`,
|
|
1056
|
+
name,
|
|
1057
|
+
target: 'body',
|
|
1058
|
+
schema: property.schema,
|
|
1059
|
+
required: schema.required.has(name),
|
|
1060
|
+
description: property.schema.description,
|
|
1061
|
+
}),
|
|
1062
|
+
{
|
|
1063
|
+
multipart: {
|
|
1064
|
+
name,
|
|
1065
|
+
isFile: property.schema.kind === 'string' &&
|
|
1066
|
+
property.schema.format === 'binary',
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
),
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
const schemaType = this.getSchemaType(ctx, { schema });
|
|
1075
|
+
const name = !schemaType || /^Any\??$/.test(schemaType.name)
|
|
1076
|
+
? 'body'
|
|
1077
|
+
: SourceBuilder.build((b) => kt.reference.write(b, schemaType));
|
|
1078
|
+
parameters.push(
|
|
1079
|
+
this.createApiParameter({
|
|
1080
|
+
id: 'body',
|
|
1081
|
+
name,
|
|
1082
|
+
target: 'body',
|
|
1083
|
+
schema,
|
|
1084
|
+
required: endpoint.requestBody.required,
|
|
1085
|
+
description: endpoint.requestBody.description,
|
|
1086
|
+
}),
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return parameters;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private createApiParameter(
|
|
1095
|
+
data: Partial<ApiParameter> & Pick<ApiParameter, 'id' | 'name' | 'target'>,
|
|
1096
|
+
): ApiParameter {
|
|
1097
|
+
return {
|
|
1098
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1099
|
+
$src: undefined!,
|
|
1100
|
+
$ref: undefined,
|
|
1101
|
+
schema: undefined,
|
|
1102
|
+
required: false,
|
|
1103
|
+
description: undefined,
|
|
1104
|
+
allowEmptyValue: undefined,
|
|
1105
|
+
allowReserved: undefined,
|
|
1106
|
+
deprecated: false,
|
|
1107
|
+
explode: undefined,
|
|
1108
|
+
style: undefined,
|
|
1109
|
+
...data,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
export function adjustListType<T>(ctx: Context, type: T): T {
|
|
1115
|
+
if (!kt.refs.list.matches(type)) return type;
|
|
1116
|
+
switch (ctx.config.arrayType) {
|
|
1117
|
+
case 'flux':
|
|
1118
|
+
return kt.refs.reactor.flux([type.generics[0]]) as T;
|
|
1119
|
+
default:
|
|
1120
|
+
return type;
|
|
1121
|
+
}
|
|
1122
|
+
}
|