@goast/kotlin 0.5.0 → 0.5.1-beta.1

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