@@ -7,6 +7,7 @@ import com.avsystem.justworks.core.gen.BASE64_CLASS
77import com.avsystem.justworks.core.gen.BASE_URL
88import com.avsystem.justworks.core.gen.CLIENT
99import com.avsystem.justworks.core.gen.CREATE_HTTP_CLIENT
10+ import com.avsystem.justworks.core.gen.EXPERIMENTAL_UUID_API
1011import com.avsystem.justworks.core.gen.GENERATED_SERIALIZERS_MODULE
1112import com.avsystem.justworks.core.gen.HEADERS_FUN
1213import com.avsystem.justworks.core.gen.HTTP_CLIENT
@@ -15,23 +16,31 @@ import com.avsystem.justworks.core.gen.HTTP_REQUEST_BUILDER
1516import com.avsystem.justworks.core.gen.HTTP_RESULT
1617import com.avsystem.justworks.core.gen.HTTP_SUCCESS
1718import com.avsystem.justworks.core.gen.Hierarchy
19+ import com.avsystem.justworks.core.gen.JSON_CLASS
1820import com.avsystem.justworks.core.gen.JSON_ELEMENT
21+ import com.avsystem.justworks.core.gen.JSON_PROPERTY
1922import com.avsystem.justworks.core.gen.NameRegistry
23+ import com.avsystem.justworks.core.gen.OPT_IN
2024import com.avsystem.justworks.core.gen.OutputOptions
2125import com.avsystem.justworks.core.gen.TOKEN
2226import com.avsystem.justworks.core.gen.client.BodyGenerator.buildFunctionBody
2327import com.avsystem.justworks.core.gen.client.ParametersGenerator.buildBodyParams
2428import com.avsystem.justworks.core.gen.client.ParametersGenerator.buildNullableParameter
29+ import com.avsystem.justworks.core.gen.containsUuid
2530import com.avsystem.justworks.core.gen.invoke
2631import com.avsystem.justworks.core.gen.shared.toAuthParam
2732import com.avsystem.justworks.core.gen.toCamelCase
2833import com.avsystem.justworks.core.gen.toPascalCase
2934import com.avsystem.justworks.core.gen.toTypeName
3035import com.avsystem.justworks.core.model.ApiKeyLocation
3136import com.avsystem.justworks.core.model.ApiSpec
37+ import com.avsystem.justworks.core.model.ContentType
3238import com.avsystem.justworks.core.model.Endpoint
3339import com.avsystem.justworks.core.model.ParameterLocation
40+ import com.avsystem.justworks.core.model.Response
3441import com.avsystem.justworks.core.model.SecurityScheme
42+ import com.squareup.kotlinpoet.AnnotationSpec
43+ import com.squareup.kotlinpoet.BYTE_ARRAY
3544import com.squareup.kotlinpoet.ClassName
3645import com.squareup.kotlinpoet.CodeBlock
3746import com.squareup.kotlinpoet.FileSpec
@@ -74,12 +83,7 @@ internal object ClientGenerator {
7483 val simpleName = " ${options.apiClassPrefix}${tag.toPascalCase()}${options.apiClassSuffix} "
7584 val className = ClassName (apiPackage, nameRegistry.register(simpleName))
7685
77- val clientInitializer = if (hasPolymorphicTypes) {
78- val generatedSerializersModule = MemberName (hierarchy.modelPackage, GENERATED_SERIALIZERS_MODULE )
79- CodeBlock .of(" ${CREATE_HTTP_CLIENT } (%M)" , generatedSerializersModule)
80- } else {
81- CodeBlock .of(" ${CREATE_HTTP_CLIENT } ()" )
82- }
86+ val clientInitializer = CodeBlock .of(" ${CREATE_HTTP_CLIENT } ()" )
8387
8488 val tokenType = LambdaTypeName .get(returnType = STRING )
8589 val isSingleBearer = securitySchemes.singleOrNull() is SecurityScheme .Bearer
@@ -93,6 +97,15 @@ internal object ClientGenerator {
9397 .superclass(API_CLIENT_BASE )
9498 .addSuperclassConstructorParameter(BASE_URL )
9599
100+ if (hasPolymorphicTypes) {
101+ val generatedSerializersModule = MemberName (hierarchy.modelPackage, GENERATED_SERIALIZERS_MODULE )
102+ classBuilder.addSuperclassConstructorParameter(
103+ " $JSON_PROPERTY = %T { serializersModule = %M }" ,
104+ JSON_CLASS ,
105+ generatedSerializersModule,
106+ )
107+ }
108+
96109 if (isSingleBearer) {
97110 // Single Bearer: use plain "token" param name for ergonomics
98111 constructorBuilder.addParameter(TOKEN , tokenType)
@@ -143,10 +156,28 @@ internal object ClientGenerator {
143156 classBuilder.addFunctions(endpoints.map { generateEndpointFunction(it) })
144157 }
145158
146- return FileSpec
147- .builder(className)
148- .addType(classBuilder.build())
149- .build()
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+ )
167+ }
168+ return fileBuilder.build()
169+ }
170+
171+ // A Uuid-typed path/query/header param, request body, or response schema anywhere in this tag
172+ // group means the generated function signatures reference kotlin.uuid.Uuid directly, which
173+ // requires this file to opt into ExperimentalUuidApi (mirrors ModelGenerator's per-model check).
174+ private fun List<Endpoint>.usesUuid (): Boolean = any { endpoint ->
175+ val responseRefs = endpoint.responses.values
176+ .asSequence()
177+ .mapNotNull { it.schema }
178+ val requestRef = endpoint.requestBody?.schema
179+ val parameterRefs = endpoint.parameters.asSequence().map { it.schema }
180+ (responseRefs + listOfNotNull(requestRef) + parameterRefs).any { it.containsUuid() }
150181 }
151182
152183 private fun buildApplyAuth (
@@ -224,6 +255,7 @@ internal object ClientGenerator {
224255 private fun generateEndpointFunction(endpoint: Endpoint): FunSpec {
225256 val functionName = methodRegistry.register(endpoint.operationId.toCamelCase())
226257 val returnBodyType = resolveReturnType(endpoint)
258+ val responseContentType = resolveSuccessResponse(endpoint)?.contentType
227259 val errorType = resolveErrorType(endpoint)
228260 val returnType = HTTP_RESULT.parameterizedBy(errorType, returnBodyType)
229261
@@ -273,7 +305,7 @@ internal object ClientGenerator {
273305 }
274306 }
275307
276- funBuilder.addCode(buildFunctionBody(endpoint, params, returnBodyType))
308+ funBuilder.addCode(buildFunctionBody(endpoint, params, returnBodyType, responseContentType ))
277309
278310 return funBuilder.build()
279311 }
@@ -296,15 +328,41 @@ internal object ClientGenerator {
296328
297329 context(_: Hierarchy)
298330 private fun resolveReturnType(endpoint: Endpoint): TypeName {
299- val twoXxSchema = endpoint.responses
331+ val response = resolveSuccessResponse(endpoint) ?: return UNIT
332+ val schemaType = response.schema?.toTypeName() ?: return UNIT
333+
334+ // The declared schema type isn't always the type that can actually be decoded off the
335+ // wire for a given content type, so it's overridden with whatever IS a faithful, safely
336+ // decodable representation of that content type — rather than either forcing a decode
337+ // that throws on every call, or failing generation over a spec inconsistency:
338+ // - application/json: a `{type: string, format: byte}` (ByteArray) schema is a base64
339+ // *string* on the wire, not a JSON byte array — kotlinx.serialization's built-in
340+ // ByteArraySerializer can't decode it. Surface the (still base64-encoded) String as-is.
341+ // - text/plain is always raw text, so String is always a faithful representation of it,
342+ // regardless of what the schema claims (e.g. `type: integer`) — body<String>() always
343+ // works, and a caller wanting the parsed type can convert it themselves.
344+ // - application/octet-stream is arbitrary binary, not necessarily valid UTF-8 text, so
345+ // ByteArray is the only safe universal representation — never downgrade this one to
346+ // String, unlike text/plain, since that risks throwing or corrupting non-UTF8 bytes.
347+ return when {
348+ schemaType == BYTE_ARRAY && response.contentType == ContentType.JSON_CONTENT_TYPE -> STRING
349+ response.contentType == ContentType.TEXT_PLAIN -> STRING
350+ response.contentType == ContentType.OCTET_STREAM -> BYTE_ARRAY
351+ else -> schemaType
352+ }
353+ }
354+
355+ // The response whose schema/contentType determine the endpoint's return type: the first 2xx
356+ // response with a schema, or (only when there's no 2xx response at all) the default response.
357+ private fun resolveSuccessResponse(endpoint: Endpoint): Response? {
358+ val twoXxResponse = endpoint.responses.entries
300359 .asSequence()
301360 .filter { it.key.startsWith(" 2 " ) }
302- .firstNotNullOfOrNull { it.value.schema }
361+ .map { it.value }
362+ .firstOrNull { it.schema != null }
303363
304- val schema = twoXxSchema ?: endpoint.responses[" default" ]?.schema .takeIf {
364+ return twoXxResponse ?: endpoint.responses[" default" ]?.takeIf {
305365 endpoint.responses.none { it.key.startsWith(" 2 " ) }
306366 }
307-
308- return schema?.toTypeName() ?: UNIT
309367 }
310368}
0 commit comments