@@ -67,21 +67,25 @@ internal object ClientGenerator {
6767 context(_: Hierarchy , _: OutputOptions , _: ApiPackage , _: NameRegistry )
6868 fun generate (spec : ApiSpec , hasPolymorphicTypes : Boolean ): List <FileSpec > {
6969 val grouped = spec.endpoints.groupBy { it.tags.firstOrNull() ? : DEFAULT_TAG }
70- return grouped.map { (tag, endpoints) ->
71- generateClientFile (tag, endpoints, hasPolymorphicTypes, spec.securitySchemes, spec.title)
70+ return grouped.flatMap { (tag, endpoints) ->
71+ generateClientFiles (tag, endpoints, hasPolymorphicTypes, spec.securitySchemes, spec.title)
7272 }
7373 }
7474
7575 context(hierarchy: Hierarchy , options: OutputOptions , apiPackage: ApiPackage , nameRegistry: NameRegistry )
76- private fun generateClientFile (
76+ private fun generateClientFiles (
7777 tag : String ,
7878 endpoints : List <Endpoint >,
7979 hasPolymorphicTypes : Boolean ,
8080 securitySchemes : List <SecurityScheme >,
8181 specTitle : String ,
82- ): FileSpec {
83- val simpleName = " ${options.apiClassPrefix}${tag.toPascalCase()}${options.apiClassSuffix} "
84- val className = ClassName (apiPackage, nameRegistry.register(simpleName))
82+ ): List <FileSpec > {
83+ val baseName = " ${options.apiClassPrefix}${tag.toPascalCase()}${options.apiClassSuffix} "
84+
85+ val interfaceClass =
86+ if (options.generateInterfaces) ClassName (apiPackage, nameRegistry.register(baseName)) else null
87+ val classSimpleName = if (options.generateInterfaces) " ${baseName} Impl" else baseName
88+ val className = ClassName (apiPackage, nameRegistry.register(classSimpleName))
8589
8690 val clientInitializer = CodeBlock .of(" ${CREATE_HTTP_CLIENT } ()" )
8791
@@ -97,6 +101,10 @@ internal object ClientGenerator {
97101 .superclass(API_CLIENT_BASE )
98102 .addSuperclassConstructorParameter(BASE_URL )
99103
104+ if (interfaceClass != null ) {
105+ classBuilder.addSuperinterface(interfaceClass)
106+ }
107+
100108 if (hasPolymorphicTypes) {
101109 val generatedSerializersModule = MemberName (hierarchy.modelPackage, GENERATED_SERIALIZERS_MODULE )
102110 classBuilder.addSuperclassConstructorParameter(
@@ -153,21 +161,54 @@ internal object ClientGenerator {
153161 }
154162
155163 context(NameRegistry ()) {
156- classBuilder.addFunctions(endpoints.map { generateEndpointFunction(it) })
164+ classBuilder.addFunctions(
165+ endpoints.map {
166+ generateEndpointFunction(
167+ it,
168+ override = options.generateInterfaces,
169+ includeBody = true ,
170+ includeKdoc = options.generateKdoc && ! options.generateInterfaces,
171+ )
172+ },
173+ )
157174 }
158175
159- val fileBuilder = FileSpec .builder(className).addType(classBuilder.build())
160- if (endpoints.usesUuid()) {
161- fileBuilder.addAnnotation(
162- AnnotationSpec
163- .builder(OPT_IN )
164- .addMember(" %T::class" , EXPERIMENTAL_UUID_API )
165- .build(),
166- )
176+ val usesUuid = endpoints.usesUuid()
177+
178+ val classFileBuilder = FileSpec .builder(className).addType(classBuilder.build())
179+ if (usesUuid) {
180+ classFileBuilder.addAnnotation(buildUuidOptInAnnotation())
181+ }
182+
183+ val interfaceFile = interfaceClass?.let {
184+ val interfaceBuilder = TypeSpec .interfaceBuilder(interfaceClass)
185+ context(NameRegistry ()) {
186+ interfaceBuilder.addFunctions(
187+ endpoints.map {
188+ generateEndpointFunction(
189+ it,
190+ override = false ,
191+ includeBody = false ,
192+ includeKdoc = options.generateKdoc,
193+ )
194+ },
195+ )
196+ }
197+ val interfaceFileBuilder = FileSpec .builder(interfaceClass).addType(interfaceBuilder.build())
198+ if (usesUuid) {
199+ interfaceFileBuilder.addAnnotation(buildUuidOptInAnnotation())
200+ }
201+ interfaceFileBuilder.build()
167202 }
168- return fileBuilder.build()
203+
204+ return listOfNotNull(interfaceFile, classFileBuilder.build())
169205 }
170206
207+ private fun buildUuidOptInAnnotation (): AnnotationSpec = AnnotationSpec
208+ .builder(OPT_IN )
209+ .addMember(" %T::class" , EXPERIMENTAL_UUID_API )
210+ .build()
211+
171212 // A Uuid-typed path/query/header param, request body, or response schema anywhere in this tag
172213 // group means the generated function signatures reference kotlin.uuid.Uuid directly, which
173214 // requires this file to opt into ExperimentalUuidApi (mirrors ModelGenerator's per-model check).
@@ -251,8 +292,21 @@ internal object ClientGenerator {
251292 return builder.build()
252293 }
253294
254- context(_: Hierarchy, options: OutputOptions, methodRegistry: NameRegistry)
255- private fun generateEndpointFunction(endpoint: Endpoint): FunSpec {
295+ /**
296+ * Builds a suspend function for an endpoint.
297+ *
298+ * @param override emit an `override` modifier (the client implements an interface)
299+ * @param includeBody emit the request-building body; when false the function is abstract
300+ * (used for interface members)
301+ * @param includeKdoc emit KDoc on the function
302+ */
303+ context(_: Hierarchy, _: OutputOptions, methodRegistry: NameRegistry)
304+ private fun generateEndpointFunction(
305+ endpoint: Endpoint,
306+ override: Boolean,
307+ includeBody: Boolean,
308+ includeKdoc: Boolean,
309+ ): FunSpec {
256310 val functionName = methodRegistry.register(endpoint.operationId.toCamelCase())
257311 val returnBodyType = resolveReturnType(endpoint)
258312 val responseContentType = resolveSuccessResponse(endpoint)?.contentType
@@ -262,6 +316,7 @@ internal object ClientGenerator {
262316 val funBuilder = FunSpec
263317 .builder(functionName)
264318 .addModifiers(KModifier.SUSPEND)
319+ .apply { if (override) addModifiers(KModifier.OVERRIDE) }
265320 .returns(returnType)
266321
267322 val params = endpoint.parameters.groupBy { it.location }
@@ -278,13 +333,15 @@ internal object ClientGenerator {
278333 buildNullableParameter(param.schema, param.name, param.required)
279334 }
280335
281- funBuilder.addParameters(pathParams + queryParams + headerParams)
336+ val bodyParams = endpoint.requestBody?.let { buildBodyParams(it) }.orEmpty()
337+ val allParams = pathParams + queryParams + headerParams + bodyParams
282338
283- if (endpoint.requestBody != null) {
284- funBuilder.addParameters(buildBodyParams(endpoint.requestBody))
285- }
339+ // An overriding function may not repeat default values; defaults live on the interface.
340+ funBuilder.addParameters(
341+ if (override) allParams.map { it.toBuilder().defaultValue(null as CodeBlock?).build() } else allParams,
342+ )
286343
287- if (options.generateKdoc ) {
344+ if (includeKdoc ) {
288345 val kdocParts = mutableListOf<String>()
289346 endpoint.summary?.let { kdocParts.add(it) }
290347 endpoint.description?.let {
@@ -305,7 +362,11 @@ internal object ClientGenerator {
305362 }
306363 }
307364
308- funBuilder.addCode(buildFunctionBody(endpoint, params, returnBodyType, responseContentType))
365+ if (includeBody) {
366+ funBuilder.addCode(buildFunctionBody(endpoint, params, returnBodyType, responseContentType))
367+ } else {
368+ funBuilder.addModifiers(KModifier.ABSTRACT)
369+ }
309370
310371 return funBuilder.build()
311372 }
0 commit comments