Skip to content

Commit bc68f5b

Browse files
authored
Merge pull request #10 from AVSystem/feat/client-generation
feat: add client generation
2 parents c6f4ca8 + c9d30a8 commit bc68f5b

8 files changed

Lines changed: 1168 additions & 1 deletion

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package com.avsystem.justworks.core.gen
2+
3+
import com.squareup.kotlinpoet.CodeBlock
4+
import com.squareup.kotlinpoet.ContextParameter
5+
import com.squareup.kotlinpoet.ExperimentalKotlinPoetApi
6+
import com.squareup.kotlinpoet.FileSpec
7+
import com.squareup.kotlinpoet.FunSpec
8+
import com.squareup.kotlinpoet.KModifier
9+
import com.squareup.kotlinpoet.LambdaTypeName
10+
import com.squareup.kotlinpoet.ParameterSpec
11+
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
12+
import com.squareup.kotlinpoet.PropertySpec
13+
import com.squareup.kotlinpoet.STRING
14+
import com.squareup.kotlinpoet.TypeSpec
15+
import com.squareup.kotlinpoet.TypeVariableName
16+
import com.squareup.kotlinpoet.UNIT
17+
18+
/**
19+
* Generates the shared `ApiClientBase.kt` file containing:
20+
* - `encodeParam<T>()` top-level utility function
21+
* - `HttpResponse.mapToResult<T>()` private extension with response mapping logic
22+
* - `HttpResponse.toResult<T>()` extension for typed response mapping
23+
* - `HttpResponse.toEmptyResult()` extension for Unit response mapping
24+
* - `ApiClientBase` abstract class with common client infrastructure
25+
*/
26+
@OptIn(ExperimentalKotlinPoetApi::class)
27+
object ApiClientBaseGenerator {
28+
private const val BLOCK = "block"
29+
private const val MAP_TO_RESULT = "mapToResult"
30+
private const val SUCCESS_BODY = "successBody"
31+
private const val SERIALIZERS_MODULE_PARAM = "serializersModule"
32+
33+
fun generate(): FileSpec {
34+
val t = TypeVariableName("T").copy(reified = true)
35+
36+
return FileSpec
37+
.builder(API_CLIENT_BASE)
38+
.addFunction(buildEncodeParam(t))
39+
.addFunction(buildMapToResult(t))
40+
.addFunction(buildToResult(t))
41+
.addFunction(buildToEmptyResult())
42+
.addType(buildApiClientBaseClass())
43+
.build()
44+
}
45+
46+
private fun buildEncodeParam(t: TypeVariableName): FunSpec = FunSpec
47+
.builder("encodeParam")
48+
.addModifiers(KModifier.INLINE)
49+
.addTypeVariable(t)
50+
.addParameter("value", TypeVariableName("T"))
51+
.returns(STRING)
52+
.addStatement("return %T.%M(value).trim('\"')", JSON_CLASS, ENCODE_TO_STRING_FUN)
53+
.build()
54+
55+
private fun buildMapToResult(t: TypeVariableName): FunSpec = FunSpec
56+
.builder(MAP_TO_RESULT)
57+
.addModifiers(KModifier.PRIVATE, KModifier.SUSPEND, KModifier.INLINE)
58+
.addTypeVariable(t)
59+
.receiver(HTTP_RESPONSE)
60+
.contextParameters(listOf(ContextParameter(RAISE.parameterizedBy(HTTP_ERROR))))
61+
.addParameter(SUCCESS_BODY, LambdaTypeName.get(returnType = TypeVariableName("T")))
62+
.returns(HTTP_SUCCESS.parameterizedBy(TypeVariableName("T")))
63+
.beginControlFlow("return when (status.value)")
64+
.addStatement("in 200..299 -> %T(status.value, %L())", HTTP_SUCCESS, SUCCESS_BODY)
65+
.addStatement(
66+
"in 300..399 -> %M(%T(status.value, %M(), %T.Redirect))",
67+
RAISE_FUN,
68+
HTTP_ERROR,
69+
BODY_AS_TEXT_FUN,
70+
HTTP_ERROR_TYPE,
71+
).addStatement(
72+
"in 400..499 -> %M(%T(status.value, %M(), %T.Client))",
73+
RAISE_FUN,
74+
HTTP_ERROR,
75+
BODY_AS_TEXT_FUN,
76+
HTTP_ERROR_TYPE,
77+
).addStatement(
78+
"else -> %M(%T(status.value, %M(), %T.Server))",
79+
RAISE_FUN,
80+
HTTP_ERROR,
81+
BODY_AS_TEXT_FUN,
82+
HTTP_ERROR_TYPE,
83+
).endControlFlow()
84+
.build()
85+
86+
private fun buildToResult(t: TypeVariableName): FunSpec = FunSpec
87+
.builder("toResult")
88+
.addModifiers(KModifier.SUSPEND, KModifier.INLINE)
89+
.addTypeVariable(t)
90+
.receiver(HTTP_RESPONSE)
91+
.contextParameters(listOf(ContextParameter(RAISE.parameterizedBy(HTTP_ERROR))))
92+
.returns(HTTP_SUCCESS.parameterizedBy(TypeVariableName("T")))
93+
.addStatement("return %L { %M() }", MAP_TO_RESULT, BODY_FUN)
94+
.build()
95+
96+
private fun buildToEmptyResult(): FunSpec = FunSpec
97+
.builder("toEmptyResult")
98+
.addModifiers(KModifier.SUSPEND)
99+
.receiver(HTTP_RESPONSE)
100+
.contextParameters(listOf(ContextParameter(RAISE.parameterizedBy(HTTP_ERROR))))
101+
.returns(HTTP_SUCCESS.parameterizedBy(UNIT))
102+
.addStatement("return %L { Unit }", MAP_TO_RESULT)
103+
.build()
104+
105+
private fun buildApiClientBaseClass(): TypeSpec {
106+
val tokenType = LambdaTypeName.get(returnType = STRING)
107+
108+
val constructor = FunSpec
109+
.constructorBuilder()
110+
.addParameter(BASE_URL, STRING)
111+
.addParameter(TOKEN, tokenType)
112+
.build()
113+
114+
val baseUrlProp = PropertySpec
115+
.builder(BASE_URL, STRING)
116+
.initializer(BASE_URL)
117+
.addModifiers(KModifier.PROTECTED)
118+
.build()
119+
120+
val tokenProp = PropertySpec
121+
.builder(TOKEN, tokenType)
122+
.initializer(TOKEN)
123+
.addModifiers(KModifier.PRIVATE)
124+
.build()
125+
126+
val clientProp = PropertySpec
127+
.builder(CLIENT, HTTP_CLIENT)
128+
.addModifiers(KModifier.PROTECTED, KModifier.ABSTRACT)
129+
.build()
130+
131+
val closeFun = FunSpec
132+
.builder("close")
133+
.addModifiers(KModifier.OVERRIDE)
134+
.addStatement("$CLIENT.close()")
135+
.build()
136+
137+
return TypeSpec
138+
.classBuilder(API_CLIENT_BASE)
139+
.addModifiers(KModifier.ABSTRACT)
140+
.addSuperinterface(CLOSEABLE)
141+
.primaryConstructor(constructor)
142+
.addProperty(baseUrlProp)
143+
.addProperty(tokenProp)
144+
.addProperty(clientProp)
145+
.addFunction(closeFun)
146+
.addFunction(buildApplyAuth())
147+
.addFunction(buildSafeCall())
148+
.addFunction(buildCreateHttpClient())
149+
.build()
150+
}
151+
152+
private fun buildApplyAuth(): FunSpec = FunSpec
153+
.builder(APPLY_AUTH)
154+
.addModifiers(KModifier.PROTECTED)
155+
.receiver(HTTP_REQUEST_BUILDER)
156+
.beginControlFlow("%M", HEADERS_FUN)
157+
.addStatement(
158+
"append(%T.Authorization, %P)",
159+
HTTP_HEADERS,
160+
CodeBlock.of($$"Bearer ${'$'}{$$TOKEN()}"),
161+
).endControlFlow()
162+
.build()
163+
164+
private fun buildSafeCall(): FunSpec = FunSpec
165+
.builder(SAFE_CALL)
166+
.addModifiers(KModifier.PROTECTED, KModifier.SUSPEND)
167+
.contextParameters(listOf(ContextParameter(RAISE.parameterizedBy(HTTP_ERROR))))
168+
.addParameter(BLOCK, LambdaTypeName.get(returnType = HTTP_RESPONSE).copy(suspending = true))
169+
.returns(HTTP_RESPONSE)
170+
.beginControlFlow("return try")
171+
.addStatement("%L()", BLOCK)
172+
.nextControlFlow("catch (e: %T)", IO_EXCEPTION)
173+
.addStatement(
174+
"%M(%T(0, e.message ?: %S, %T.Network))",
175+
RAISE_FUN,
176+
HTTP_ERROR,
177+
NETWORK_ERROR,
178+
HTTP_ERROR_TYPE,
179+
).nextControlFlow("catch (e: %T)", HTTP_REQUEST_TIMEOUT_EXCEPTION)
180+
.addStatement(
181+
"%M(%T(0, e.message ?: %S, %T.Network))",
182+
RAISE_FUN,
183+
HTTP_ERROR,
184+
NETWORK_ERROR,
185+
HTTP_ERROR_TYPE,
186+
).endControlFlow()
187+
.build()
188+
189+
private fun buildCreateHttpClient(): FunSpec = FunSpec
190+
.builder(CREATE_HTTP_CLIENT)
191+
.addModifiers(KModifier.PROTECTED)
192+
.addParameter(
193+
ParameterSpec
194+
.builder(SERIALIZERS_MODULE_PARAM, SERIALIZERS_MODULE.copy(nullable = true))
195+
.defaultValue("null")
196+
.build(),
197+
).returns(HTTP_CLIENT)
198+
.beginControlFlow("return %T", HTTP_CLIENT)
199+
.beginControlFlow("install(%T)", CONTENT_NEGOTIATION)
200+
.beginControlFlow("if ($SERIALIZERS_MODULE_PARAM != null)")
201+
.addStatement(
202+
"%M(%T { this.$SERIALIZERS_MODULE_PARAM = $SERIALIZERS_MODULE_PARAM })",
203+
JSON_FUN,
204+
JSON_CLASS,
205+
).nextControlFlow("else")
206+
.addStatement("%M()", JSON_FUN)
207+
.endControlFlow()
208+
.endControlFlow()
209+
.addStatement("expectSuccess = false")
210+
.endControlFlow()
211+
.build()
212+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.avsystem.justworks.core.gen
2+
3+
import com.squareup.kotlinpoet.FileSpec
4+
import com.squareup.kotlinpoet.FunSpec
5+
import com.squareup.kotlinpoet.INT
6+
import com.squareup.kotlinpoet.KModifier
7+
import com.squareup.kotlinpoet.PropertySpec
8+
import com.squareup.kotlinpoet.STRING
9+
import com.squareup.kotlinpoet.TypeSpec
10+
import com.squareup.kotlinpoet.TypeVariableName
11+
12+
/**
13+
* Generates [FileSpec]s containing:
14+
* - `HttpErrorType` enum class with Client, Server, Network values
15+
* - `HttpError` data class with code, message, type fields
16+
* - `HttpSuccess<T>` data class wrapping successful responses
17+
*/
18+
object ApiResponseGenerator {
19+
private const val CODE = "code"
20+
private const val MESSAGE = "message"
21+
private const val TYPE = "type"
22+
23+
fun generateHttpError(): FileSpec {
24+
val enumType = TypeSpec
25+
.enumBuilder(HTTP_ERROR_TYPE)
26+
.addEnumConstant("Client")
27+
.addEnumConstant("Server")
28+
.addEnumConstant("Redirect")
29+
.addEnumConstant("Network")
30+
.build()
31+
32+
val primaryConstructor = FunSpec
33+
.constructorBuilder()
34+
.addParameter(CODE, INT)
35+
.addParameter(MESSAGE, STRING)
36+
.addParameter(TYPE, HTTP_ERROR_TYPE)
37+
.build()
38+
39+
val dataClassType = TypeSpec
40+
.classBuilder(HTTP_ERROR)
41+
.addModifiers(KModifier.DATA)
42+
.primaryConstructor(primaryConstructor)
43+
.addProperty(PropertySpec.builder(CODE, INT).initializer(CODE).build())
44+
.addProperty(PropertySpec.builder(MESSAGE, STRING).initializer(MESSAGE).build())
45+
.addProperty(PropertySpec.builder(TYPE, HTTP_ERROR_TYPE).initializer(TYPE).build())
46+
.build()
47+
48+
return FileSpec
49+
.builder(HTTP_ERROR)
50+
.addType(enumType)
51+
.addType(dataClassType)
52+
.build()
53+
}
54+
55+
fun generateHttpSuccess(): FileSpec {
56+
val t = TypeVariableName("T")
57+
58+
val primaryConstructor = FunSpec
59+
.constructorBuilder()
60+
.addParameter(CODE, INT)
61+
.addParameter(BODY, t)
62+
.build()
63+
64+
val successType = TypeSpec
65+
.classBuilder(HTTP_SUCCESS)
66+
.addModifiers(KModifier.DATA)
67+
.addTypeVariable(t)
68+
.primaryConstructor(primaryConstructor)
69+
.addProperty(PropertySpec.builder(CODE, INT).initializer(CODE).build())
70+
.addProperty(PropertySpec.builder(BODY, t).initializer(BODY).build())
71+
.build()
72+
73+
return FileSpec
74+
.builder(HTTP_SUCCESS)
75+
.addType(successType)
76+
.build()
77+
}
78+
}

0 commit comments

Comments
 (0)