Skip to content

Commit c268edd

Browse files
Test Coverage Added
1 parent 08fbc76 commit c268edd

File tree

2 files changed

+29
-52
lines changed

2 files changed

+29
-52
lines changed

client/src/test/kotlin/io/github/hosseinkarami_dev/near/rpc/client/ExperimentalCongestionLevelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ class ExperimentalCongestionLevelTest {
4444
CryptoHash("J2VFLWXq9TbzgWKitwT99pZtqxQ1E6hdXLezUyAPrJFn")))
4545
val result = response.getResultOrNull<RpcCongestionLevelResponse>()
4646
println("Experimental Congestion Response: $result")
47-
assertTrue { true }
47+
assertTrue { response is RpcResponse.Success }
4848
}
4949
}

generator/src/main/kotlin/io/github/hosseinkarami_dev/near/rpc/generator/SerializerGenerator.kt

Lines changed: 28 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package 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
49
import 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
713
import kotlinx.serialization.SerializationException
8-
914
import java.io.File
1015

1116
object 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

Comments
 (0)