Skip to content

Commit e19349d

Browse files
committed
Add OpenAPI client generation task and dependencies
1 parent d404a44 commit e19349d

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

build-logic/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ repositories {
1010
dependencies {
1111
implementation(libs.kotlin.gradle.plugin)
1212
implementation(libs.kotlin.serialization)
13+
implementation(libs.serialization.json)
1314
implementation(libs.mavenPublishing)
15+
implementation("com.squareup:kotlinpoet:2.2.0")
16+
implementation("com.charleskorn.kaml:kaml:0.102.0")
1417
}
1518

1619
kotlin {
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import com.charleskorn.kaml.Yaml
2+
import com.charleskorn.kaml.YamlMap
3+
import com.charleskorn.kaml.yamlMap
4+
import com.squareup.kotlinpoet.*
5+
import kotlinx.serialization.decodeFromString
6+
7+
plugins {
8+
id("ton-kotlin.base")
9+
}
10+
11+
abstract class GenerateOpenApiClientTask : DefaultTask() {
12+
@get:InputFile
13+
abstract val openApiSpec: RegularFileProperty
14+
15+
@get:OutputDirectory
16+
abstract val outputDir: DirectoryProperty
17+
18+
@get:Input
19+
abstract val packageName: Property<String>
20+
21+
@get:Input
22+
abstract val mainClassName: Property<String>
23+
24+
@TaskAction
25+
fun generate() {
26+
val specFile = openApiSpec.get().asFile
27+
val outDir = outputDir.get().asFile
28+
outDir.mkdirs()
29+
30+
val map = Yaml.default.decodeFromString<YamlMap>(specFile.readText())
31+
val schemas = map.get<YamlMap>("components")?.get<YamlMap>("schemas") ?: return
32+
val objects = HashMap<String, TypeSpec.Builder>()
33+
schemas.entries.forEach { modelName, modelObj ->
34+
objects.parseObjects(modelName.content, modelObj.yamlMap)
35+
}
36+
objects.forEach { (name, builder) ->
37+
builder.generateObject(schemas.get<YamlMap>(name) ?: return@forEach, schemas)
38+
val fileSpec = FileSpec.builder(packageName.get(), name)
39+
.addType(builder.build())
40+
.build()
41+
42+
fileSpec.writeTo(outDir)
43+
}
44+
45+
// val definitions = spec["definitions"]?.jsonObject ?: return
46+
//
47+
// val mainClassName = mainClassName.get()
48+
// val fileSpec = FileSpec.builder(packageName.get(), mainClassName)
49+
// .addType(
50+
// TypeSpec.interfaceBuilder(mainClassName).apply {
51+
// definitions.forEach { (name, definition) ->
52+
// val objClass = objectGen(name, definition.jsonObject)
53+
// if (objClass != null) {
54+
// addType(objClass)
55+
// }
56+
// }
57+
// }.build()
58+
// ).build()
59+
60+
}
61+
62+
fun MutableMap<String, TypeSpec.Builder>.parseObjects(modelName: String, modelObj: YamlMap) {
63+
if (modelObj.getScalar("type")?.content != "object") {
64+
return
65+
}
66+
val className = fixClassName(modelName)
67+
this[modelName] = TypeSpec.classBuilder(className)
68+
}
69+
70+
fun TypeSpec.Builder.generateObject(modelObj: YamlMap, schemasYml: YamlMap) {
71+
val properties = modelObj.get<YamlMap>("properties") ?: return
72+
val constructorBuilder = FunSpec.constructorBuilder()
73+
74+
properties.entries.forEach { propertyNameScalar, propertyYml ->
75+
val propertyName = propertyNameScalar.content
76+
val propertyYmlMap = propertyYml.yamlMap
77+
val ref = propertyYmlMap.getScalar($$"$ref")?.content
78+
val typeSpec = if (ref != null) {
79+
val refKey = ref.removePrefix("#/components/schemas/")
80+
val refSchema = schemasYml.get<YamlMap>(refKey)
81+
if (refSchema != null) {
82+
val refSchemaType = refSchema.getScalar("type")?.content
83+
if (refSchemaType == "object") {
84+
ClassName(packageName.get(), refKey)
85+
} else {
86+
typeName(refSchemaType)
87+
}
88+
} else {
89+
null
90+
}
91+
} else {
92+
val type = propertyYmlMap.getScalar("type")?.content ?: return@forEach
93+
typeName(type)
94+
}
95+
96+
if (typeSpec != null) {
97+
constructorBuilder.addParameter(
98+
propertyName, typeSpec
99+
).addModifiers(KModifier.VALUE)
100+
}
101+
}
102+
103+
primaryConstructor(constructorBuilder.build())
104+
}
105+
106+
// fun objectGen(name: String, definition: JsonObject): TypeSpec? {
107+
// if (definition["type"]?.jsonPrimitive?.content != "object") return null
108+
// return TypeSpec.classBuilder(fixClassName(name)).apply {
109+
// val properties = definition["properties"]?.jsonObject ?: return null
110+
//
111+
// properties.forEach { (propertyName, property) ->
112+
// val propertyType = toTypeName(property.jsonObject)
113+
// val camelCaseName = snakeToCamelCase(propertyName)
114+
// addProperty(PropertySpec.builder(camelCaseName, propertyType).apply {
115+
// addAnnotation(
116+
// AnnotationSpec.builder(SerialName::class)
117+
// .addMember("%S", propertyName)
118+
// .build()
119+
// )
120+
// addAnnotation(
121+
// AnnotationSpec.builder(JvmName::class)
122+
// .useSiteTarget(UseSiteTarget.GET)
123+
// .addMember("%S", camelCaseName)
124+
// .build()
125+
// )
126+
// }.build())
127+
// }
128+
// }.build()
129+
// }
130+
//
131+
// private fun toTypeName(obj: JsonObject): TypeName {
132+
// val refPath = obj[$$"$ref"]?.jsonPrimitive?.content
133+
// if (refPath != null) {
134+
// return ClassName(packageName.get(), mainClassName.get(), fixClassName(refPath.removePrefix("#/definitions/")))
135+
// }
136+
//
137+
// val typeName = obj["type"]?.jsonPrimitive?.content ?: return Any::class.asTypeName()
138+
// return when (typeName) {
139+
// "object" -> {
140+
// val additionalProperties = obj["additionalProperties"] ?: return Any::class.asTypeName()
141+
// if (additionalProperties is JsonPrimitive) {
142+
// return JsonObject::class.asTypeName()
143+
// }
144+
//
145+
// val valueType = toTypeName(additionalProperties.jsonObject)
146+
//
147+
// Map::class.asClassName()
148+
// .parameterizedBy(
149+
// String::class.asTypeName(),
150+
// valueType
151+
// )
152+
// }
153+
//
154+
// "array" -> {
155+
// val items = obj["items"]!!.jsonObject
156+
// val itemsType = toTypeName(items)
157+
// List::class.asClassName().parameterizedBy(itemsType)
158+
// }
159+
//
160+
// else -> typeName(typeName)
161+
// }
162+
// }
163+
164+
private fun typeName(type: String?): TypeName {
165+
return when (type) {
166+
"string" -> String::class.asTypeName()
167+
"integer" -> Int::class.asTypeName()
168+
"boolean" -> Boolean::class.asTypeName()
169+
else -> Any::class.asTypeName()
170+
}
171+
}
172+
173+
private fun fixClassName(name: String) = name.replace(".", "")
174+
.replaceFirstChar {
175+
it.uppercaseChar()
176+
}
177+
178+
private fun snakeToCamelCase(input: String): String {
179+
if (input.isEmpty()) return input
180+
val sb = StringBuilder(input.length)
181+
var capitalizeNext = false
182+
for (ch in input) {
183+
if (ch == '_') {
184+
capitalizeNext = true
185+
} else {
186+
if (capitalizeNext) {
187+
sb.append(ch.uppercaseChar())
188+
capitalizeNext = false
189+
} else {
190+
sb.append(ch)
191+
}
192+
}
193+
}
194+
return sb.toString()
195+
}
196+
}

0 commit comments

Comments
 (0)