11package io.github.hosseinkarami_dev.near.rpc.generator
22
3- import com.squareup.kotlinpoet.*
3+ import com.squareup.kotlinpoet.ClassName
4+ import com.squareup.kotlinpoet.CodeBlock
5+ import com.squareup.kotlinpoet.FileSpec
6+ import com.squareup.kotlinpoet.FunSpec
7+ import com.squareup.kotlinpoet.KModifier
8+ import com.squareup.kotlinpoet.MemberName
49import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
5- import io.github.hosseinkarami_dev.near.rpc.generator.SealedInfo
6- import io.github.hosseinkarami_dev.near.rpc.generator.VariantInfo
10+ import com.squareup.kotlinpoet.PropertySpec
11+ import com.squareup.kotlinpoet.TypeName
12+ import com.squareup.kotlinpoet.TypeSpec
713import kotlinx.serialization.SerializationException
8-
914import java.io.File
1015
1116object SerializerGenerator {
@@ -39,10 +44,7 @@ object SerializerGenerator {
3944 val clsName = info.className
4045 val serializerName = " ${clsName} Serializer"
4146
42- // --- smarter discriminator candidate selection ---
43- // 1) If *every* variant contains a serialized property named "name" -> prioritize it
44- // 2) Otherwise collect candidates among properties that are string-like, enum-like (Name / *Name) or simple token types
45- // exclude obvious message field unless it's name-like
47+ // --- discriminator candidate selection ---
4648 val discCandidates: List <String > = run {
4749 val nVariants = info.variants.size
4850
@@ -61,7 +63,6 @@ object SerializerGenerator {
6163 val isSimpleToken = raw.matches(Regex (" ^[A-Za-z_][A-Za-z0-9_]*$" )) && ! raw.contains(' .' ) && ! raw.contains(' <' ) && raw.length <= 60
6264
6365 if (isString || isNameLike || isSimpleToken) {
64- // avoid picking generic "message" text as discriminator unless it actually looks name-like
6566 if (p.serialName.equals(" message" , ignoreCase = true ) && ! isNameLike) continue
6667 freq[p.serialName] = (freq[p.serialName] ? : 0 ) + 1
6768 }
@@ -176,10 +177,8 @@ object SerializerGenerator {
176177 val keyToVariants = mutableMapOf<String , MutableList <Int >>()
177178 dataVariants.forEachIndexed { i, v -> v.props.forEach { p -> keyToVariants.computeIfAbsent(p.serialName) { mutableListOf () }.add(i) } }
178179 val dataVariantHasUniqueKey = dataVariants.mapIndexed { idx, v -> v.props.any { p -> keyToVariants[p.serialName]?.size == 1 } }
179- // fieldBased == true when every data-variant has at least one property that is unique to it
180180 val fieldBased = dataVariants.isNotEmpty() && dataVariantHasUniqueKey.all { it } && dataVariants.isNotEmpty()
181181
182- // Map variantName -> unique key (the first unique property serialName, or null)
183182 val uniqueKeyByVariantName = mutableMapOf<String , String ?>()
184183 dataVariants.forEach { v ->
185184 val unique = v.props.firstOrNull { p -> keyToVariants[p.serialName]?.size == 1 }?.serialName
@@ -189,16 +188,14 @@ object SerializerGenerator {
189188 val objBuilder = TypeSpec .objectBuilder(serializerName)
190189 .addSuperinterface(kSerializerOfModel)
191190
192- // --- build a descriptor with one element per variant (names = serialName)
191+ // descriptor
193192 val descriptorInitializer = CodeBlock .builder()
194193 descriptorInitializer.add(
195194 " %M(%S) {\n " ,
196195 MemberName (" kotlinx.serialization.descriptors" , " buildClassSerialDescriptor" ),
197196 " $modelsPkg .$clsName "
198197 )
199198 for (v in info.variants) {
200- val variantClass = ClassName (modelsPkg, clsName, v.name)
201- // produce: element("VariantSerialName", serializer<modelsPkg.ClsName.Variant>().descriptor)
202199 descriptorInitializer.add(
203200 " element(%S, %L)\n " ,
204201 v.serialName,
@@ -217,7 +214,6 @@ object SerializerGenerator {
217214 val variantClass = ClassName (modelsPkg, clsName, v.name)
218215 cb.beginControlFlow(" is %T ->" , variantClass)
219216
220- // --- single-value (primitive) payload
221217 if (v.props.size == 1 && v.props[0 ].name == " value" ) {
222218 val p = v.props[0 ]
223219 val ser = serializerExpressionFor(p.type, v.name)
@@ -242,8 +238,7 @@ object SerializerGenerator {
242238 cb.endControlFlow()
243239 return cb.build()
244240 }
245- // --- multi-field payload
246- // build mutable map and add entries conditionally for nullable fields
241+
247242 cb.add(" val map = %T<String, %T>()\n " , ClassName (" kotlin.collections" , " mutableMapOf" ), ClassName (" kotlinx.serialization.json" , " JsonElement" ))
248243 v.props.forEach { p ->
249244 val ser = serializerExpressionFor(p.type, v.name)
@@ -284,7 +279,6 @@ object SerializerGenerator {
284279 scb.addStatement(" return" )
285280 scb.endControlFlow()
286281
287- // non-JSON: encode full variant serializer for each variant (descriptor elements align with variant order)
288282 scb.addStatement(" val out = encoder.beginStructure(descriptor)" )
289283 scb.beginControlFlow(" when (value)" )
290284 var idx = 0
@@ -312,7 +306,7 @@ object SerializerGenerator {
312306
313307 dcb.beginControlFlow(" when (element)" )
314308
315- // JsonPrimitive branch: try single-value variants first
309+ // JsonPrimitive branch
316310 dcb.beginControlFlow(" is %T ->" , ClassName (" kotlinx.serialization.json" , " JsonPrimitive" ))
317311 val singleValueVariants = info.variants.filter { it.kind == VariantInfo .Kind .DATA_CLASS && it.props.size == 1 && it.props[0 ].name == " value" }
318312 if (singleValueVariants.isNotEmpty()) {
@@ -337,7 +331,7 @@ object SerializerGenerator {
337331 dcb.endControlFlow()
338332 dcb.endControlFlow() // end primitive
339333
340- // JsonArray branch
334+ // JsonArray
341335 dcb.beginControlFlow(" is %T ->" , ClassName (" kotlinx.serialization.json" , " JsonArray" ))
342336 dcb.addStatement(" throw %T(%S)" , SerializationException ::class , " Unexpected JSON array while deserializing $clsName " )
343337 dcb.endControlFlow()
@@ -346,14 +340,13 @@ object SerializerGenerator {
346340 dcb.beginControlFlow(" is %T ->" , ClassName (" kotlinx.serialization.json" , " JsonObject" ))
347341 dcb.addStatement(" val jobj = element" )
348342
349- // ---------- new: field-based detection with grouping to avoid duplicate checks ----------
343+ // field-based detection (unchanged)
350344 if (fieldBased) {
351345 dcb.addStatement(" // fieldBased union: detect variant by unique field presence" )
352346 for (v in dataVariants) {
353347 val unique = uniqueKeyByVariantName[v.name]
354348 if (! unique.isNullOrBlank()) {
355349 dcb.beginControlFlow(" if (jobj[%S] != null)" , unique)
356- // single-value variant that uses 'value' property
357350 if (v.props.size == 1 && v.props[0 ].name == " value" ) {
358351 val ser = serializerExpressionFor(v.props[0 ].type, v.name)
359352 dcb.addStatement(" return %T(decoder.json.decodeFromJsonElement(%L, jobj[%S]!!))" , ClassName (modelsPkg, clsName, v.name), ser, unique)
@@ -377,9 +370,8 @@ object SerializerGenerator {
377370 }
378371 }
379372
380- // --- Group variants by their required (non-nullable) keys to avoid emitting duplicated identical checks ---
373+ // group-by- required- keys logic (unchanged)
381374 run {
382- // build groups: Map(sortedRequiredKeysList -> List<VariantInfo>)
383375 val reqGroups = mutableMapOf<List <String >, MutableList <VariantInfo >>()
384376 for (v in dataVariants) {
385377 val reqKeys = v.props.filter { ! it.type.trim().endsWith(" ?" ) }.map { it.serialName }
@@ -404,7 +396,6 @@ object SerializerGenerator {
404396 }
405397 dcb.endControlFlow()
406398 } else {
407- // ambiguous group: try to disambiguate by 'type' field if present in all variants of the group
408399 val allHaveTypeField = variantsWithSameReq.all { vv -> vv.props.any { p -> p.serialName == " type" } }
409400 dcb.beginControlFlow(" if (listOf($reqListLiteral ).all { jobj[it] != null })" )
410401 if (allHaveTypeField) {
@@ -424,18 +415,17 @@ object SerializerGenerator {
424415 dcb.endControlFlow()
425416 }
426417 dcb.addStatement(" else -> { /* not recognized by type field, fallthrough */ }" )
427- dcb.endControlFlow() // end when(tfVal)
428- dcb.endControlFlow() // end if (tfElem is JsonPrimitive)
418+ dcb.endControlFlow()
419+ dcb.endControlFlow()
429420 } else {
430- // can't disambiguate here; allow later heuristics (wrapper/flat/heuristic) to handle these cases.
431421 dcb.addStatement(" // ambiguous required-keys group; skipping disambiguation here to avoid wrong decode" )
432422 }
433- dcb.endControlFlow() // end if listOf(...).all
423+ dcb.endControlFlow()
434424 }
435425 }
436426 }
437427
438- // wrapper-style with single-key
428+ // ---------- WRAPPER-STYLE (SINGLE-KEY) ----------
439429 dcb.beginControlFlow(" if (jobj.size == 1)" )
440430 dcb.addStatement(" val entry = jobj.entries.first()" )
441431 dcb.addStatement(" val key = entry.key" )
@@ -447,25 +437,17 @@ object SerializerGenerator {
447437 if (v.props.size == 1 && v.props[0 ].name == " value" ) {
448438 val ser = serializerExpressionFor(v.props[0 ].type, v.name)
449439 dcb.beginControlFlow(" %S ->" , v.serialName)
450- // valueElem is the payload (primitive or object) — decode directly
440+ // single-value: decode payload directly (primitive or object)
451441 dcb.addStatement(" return %T(decoder.json.decodeFromJsonElement(%L, valueElem))" , variantClass, ser)
452442 dcb.endControlFlow()
453443 } else {
454444 dcb.beginControlFlow(" %S ->" , v.serialName)
445+ // IMPORTANT FIX: decode the payload object itself via the variant serializer,
446+ // do NOT assume nested field with the variant name inside the payload.
455447 dcb.addStatement(" val obj = valueElem as? %T ?: throw %T(%S + key)" , ClassName (" kotlinx.serialization.json" , " JsonObject" ), SerializationException ::class , " Expected object payload for variant " )
456- val decodeNames = mutableListOf<String >()
457- v.props.forEach { p ->
458- val ser = serializerExpressionFor(p.type, v.name)
459- val varName = p.name + " Val"
460- val nullable = p.type.trim().endsWith(" ?" )
461- if (nullable) {
462- dcb.addStatement(" val %L = obj[%S]?.let { decoder.json.decodeFromJsonElement(%L, it) }" , varName, p.serialName, ser)
463- } else {
464- dcb.addStatement(" val %L = decoder.json.decodeFromJsonElement(%L, obj[%S] ?: throw %T(%S + key))" , varName, ser, p.serialName, SerializationException ::class , " Missing field '${p.serialName} ' for variant " )
465- }
466- decodeNames + = varName
467- }
468- dcb.addStatement(" return %T(${decodeNames.joinToString(" , " )} )" , variantClass)
448+ val variantSerializerCb = CodeBlock .of(" serializer<%T>()" , ClassName (modelsPkg, clsName, v.name))
449+ // decode full variant from the payload object
450+ dcb.addStatement(" return decoder.json.decodeFromJsonElement(%L, obj)" , variantSerializerCb)
469451 dcb.endControlFlow()
470452 }
471453 }
@@ -479,7 +461,7 @@ object SerializerGenerator {
479461 dcb.endControlFlow() // end when(key)
480462 dcb.endControlFlow() // end if (jobj.size == 1)
481463
482- // flat-style: try configured discriminators first (derived from sealed info), then heuristic fallback
464+ // ---------- flat-style / heuristics (unchanged) ----------
483465 dcb.beginControlFlow(" else" )
484466
485467 dcb.addStatement(" var typeField: String? = null" )
@@ -495,7 +477,6 @@ object SerializerGenerator {
495477 dcb.endControlFlow()
496478 }
497479
498- // heuristic: if still null, look for any string value matching a known variant serialName
499480 val variantNamesList = info.variants.joinToString(" , " ) { " \" ${it.serialName} \" " }
500481 dcb.addStatement(" if (typeField == null) {" )
501482 dcb.addStatement(" val knownVariantNames = setOf($variantNamesList )" )
@@ -507,14 +488,11 @@ object SerializerGenerator {
507488 dcb.addStatement(" }" )
508489 dcb.addStatement(" }" )
509490
510- // still null -> error
511491 val discMsg = if (discCandidates.isNotEmpty()) " Missing discriminator (one of ${discCandidates.joinToString(" /" )} ) or recognizable variant in $clsName " else " Missing discriminator or recognizable variant in $clsName "
512492 dcb.addStatement(" if (typeField == null) throw %T(%S)" , SerializationException ::class , discMsg)
513493
514- // normalize typeField for safe matching
515494 dcb.addStatement(" val tf = typeField.trim()" )
516495
517- // now dispatch based on tf; match against generated variant serialNames
518496 dcb.beginControlFlow(" when (tf)" )
519497 for (v in info.variants.filter { it.kind == VariantInfo .Kind .DATA_CLASS }) {
520498 dcb.beginControlFlow(" %S ->" , v.serialName)
@@ -548,7 +526,6 @@ object SerializerGenerator {
548526 val fileSpecBuilder = FileSpec .builder(serializerPackage, serializerName)
549527 .addType(objBuilder.build())
550528 fileSpecBuilder.addImport(" kotlinx.serialization" , " serializer" )
551- // add json helper imports unconditionally (safe)
552529 fileSpecBuilder.addImport(" kotlinx.serialization.json" , " jsonPrimitive" )
553530 fileSpecBuilder.addImport(" kotlinx.serialization.json" , " contentOrNull" )
554531 return fileSpecBuilder.build()
@@ -564,4 +541,4 @@ object SerializerGenerator {
564541 if (s.startsWith(" `" ) && s.endsWith(" `" ) && s.length > 1 ) s = s.substring(1 , s.length - 1 )
565542 return s.trim()
566543 }
567- }
544+ }
0 commit comments