mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-10-08 14:48:14 +03:00
[F] Fix import/export disparity
This commit is contained in:
+54
-10
@@ -2,8 +2,10 @@ package ext
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonToken
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -21,16 +23,58 @@ val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, obj
|
||||
else -> 400 - "Invalid boolean value ${parser.text}"
|
||||
}
|
||||
})
|
||||
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
|
||||
// First try standard formats via asDateTime() method
|
||||
parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
|
||||
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
|
||||
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
|
||||
} catch (e: Exception) {
|
||||
400 - "Invalid date time value ${parser.text}"
|
||||
} }
|
||||
})
|
||||
val JSON_DATETIME = SimpleModule()
|
||||
.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
||||
.addDeserializer(LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDateTime? {
|
||||
parser.readLocalDateTimeArray(context)?.let { return it }
|
||||
|
||||
// First try standard formats via asDateTime() method
|
||||
return parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
|
||||
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
|
||||
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
|
||||
} catch (e: Exception) {
|
||||
400 - "Invalid date time value ${parser.text}"
|
||||
} }
|
||||
}
|
||||
})
|
||||
|
||||
fun JsonParser.readLocalDateTimeArray(context: DeserializationContext): LocalDateTime? {
|
||||
if (!isExpectedStartArrayToken) return null
|
||||
|
||||
val parts = mutableListOf<Int>()
|
||||
while (nextToken() != JsonToken.END_ARRAY) {
|
||||
if (currentToken() != JsonToken.VALUE_NUMBER_INT) {
|
||||
throw context.weirdStringException(
|
||||
parts.toString(),
|
||||
LocalDateTime::class.java,
|
||||
"Invalid legacy date-time array",
|
||||
)
|
||||
}
|
||||
parts += intValue
|
||||
}
|
||||
|
||||
if (parts.size !in 5..7) {
|
||||
throw context.weirdStringException(
|
||||
parts.toString(),
|
||||
LocalDateTime::class.java,
|
||||
"Invalid legacy date-time array; expected 5 to 7 integer components",
|
||||
)
|
||||
}
|
||||
|
||||
return try {
|
||||
LocalDateTime.of(
|
||||
parts[0], parts[1], parts[2], parts[3], parts[4],
|
||||
parts.getOrElse(5) { 0 }, parts.getOrElse(6) { 0 },
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw context.weirdStringException(
|
||||
parts.toString(),
|
||||
LocalDateTime::class.java,
|
||||
"Invalid legacy date-time array: ${e.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
val JACKSON = jacksonObjectMapper().apply {
|
||||
setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
|
||||
findAndRegisterModules()
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import ext.readLocalDateTimeArray
|
||||
import icu.samnyan.aqua.net.games.BaseEntity
|
||||
import icu.samnyan.aqua.net.games.IUserData
|
||||
import icu.samnyan.aqua.sega.chusan.model.request.UserEmoney
|
||||
@@ -24,6 +25,8 @@ import java.time.temporal.ChronoField
|
||||
class FlexibleDateTimeDeserializer : JsonDeserializer<LocalDateTime?>() {
|
||||
@Throws(IOException::class)
|
||||
public override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
|
||||
p.readLocalDateTimeArray(ctxt)?.let { return it }
|
||||
|
||||
val value = p.text
|
||||
return FORMATTERS.firstNotNullOfOrNull { formatter ->
|
||||
runCatching { LocalDateTime.parse(value, formatter) }.getOrNull()
|
||||
|
||||
@@ -3,6 +3,7 @@ package test
|
||||
import com.fasterxml.jackson.databind.JsonMappingException
|
||||
import ext.JACKSON
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserData
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserCharge
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserCMissionProgress
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
@@ -44,6 +45,26 @@ class Chu3ImportJsonTest : StringSpec({
|
||||
imported.lastPlayDate shouldBe LocalDateTime.of(2026, 8, 4, 1, 2, 3)
|
||||
}
|
||||
|
||||
"legacy backup date arrays remain importable" {
|
||||
val userData = JACKSON.readValue(
|
||||
"""{
|
||||
"eventWatchedDate":[2026,8,4,1,2],
|
||||
"firstPlayDate":[2026,8,4,1,2,3],
|
||||
"lastPlayDate":[2026,8,4,1,2,3,123456000]
|
||||
}""",
|
||||
Chu3UserData::class.java,
|
||||
)
|
||||
val charge = JACKSON.readValue(
|
||||
"""{"purchaseDate":[2026,8,4,1,2,3,123456000]}""",
|
||||
UserCharge::class.java,
|
||||
)
|
||||
|
||||
userData.eventWatchedDate shouldBe LocalDateTime.of(2026, 8, 4, 1, 2)
|
||||
userData.firstPlayDate shouldBe LocalDateTime.of(2026, 8, 4, 1, 2, 3)
|
||||
userData.lastPlayDate shouldBe LocalDateTime.of(2026, 8, 4, 1, 2, 3, 123456000)
|
||||
charge.purchaseDate shouldBe LocalDateTime.of(2026, 8, 4, 1, 2, 3, 123456000)
|
||||
}
|
||||
|
||||
"invalid CHUNITHM dates are rejected with a useful message" {
|
||||
val error = shouldThrow<JsonMappingException> {
|
||||
JACKSON.readValue(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package test
|
||||
|
||||
import ext.JACKSON
|
||||
import ext.parseJackson
|
||||
import icu.samnyan.aqua.net.games.chu3.Chu3DataExport
|
||||
import icu.samnyan.aqua.net.games.mai2.Maimai2DataExport
|
||||
import icu.samnyan.aqua.net.games.ongeki.OngekiDataExport
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserLinkedVerse
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserCharge
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserCourse
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserDuel
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserGacha
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserLoginBonus
|
||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserPlaylog
|
||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class ImportExportParityTest : StringSpec({
|
||||
val timestamp = LocalDateTime.of(2026, 8, 4, 1, 2, 3, 123_456_000)
|
||||
|
||||
"CHUNITHM export JSON can be imported without changing data" {
|
||||
val data = Chu3DataExport().apply {
|
||||
userData.eventWatchedDate = timestamp
|
||||
userData.firstPlayDate = timestamp
|
||||
userData.lastPlayDate = timestamp
|
||||
userChargeList = listOf(UserCharge().apply {
|
||||
purchaseDate = timestamp
|
||||
validDate = timestamp
|
||||
paramDate = timestamp
|
||||
})
|
||||
userCourseList = listOf(UserCourse().apply { lastPlayDate = timestamp })
|
||||
userDuelList = listOf(UserDuel().apply { lastPlayDate = timestamp })
|
||||
userPlaylogList = listOf(UserPlaylog().apply {
|
||||
playDate = timestamp
|
||||
userPlayDate = timestamp
|
||||
})
|
||||
userGachaList = listOf(UserGacha().apply { dailyGachaDate = timestamp })
|
||||
userLinkedVerseList = listOf(Chu3UserLinkedVerse().apply { clearDate = timestamp })
|
||||
userLoginBonusList = listOf(UserLoginBonus(lastUpdateDate = timestamp))
|
||||
}
|
||||
|
||||
assertJsonRoundTrip(data)
|
||||
val tree = JACKSON.readTree(JACKSON.writeValueAsString(data))
|
||||
tree.at("/userData/firstPlayDate").isTextual shouldBe true
|
||||
tree.at("/userChargeList/0/purchaseDate").isTextual shouldBe true
|
||||
}
|
||||
|
||||
"maimai DX export JSON can be imported without changing data" {
|
||||
val data = Maimai2DataExport().apply {
|
||||
userKaleidxScopeList = listOf(Mai2UserKaleidx().apply {
|
||||
bestAchievementDate = timestamp
|
||||
bestDeluxscoreDate = timestamp
|
||||
clearDate = timestamp
|
||||
lastPlayDate = timestamp
|
||||
})
|
||||
}
|
||||
|
||||
assertJsonRoundTrip(data)
|
||||
JACKSON.readTree(JACKSON.writeValueAsString(data))
|
||||
.at("/userKaleidxScopeList/0/lastPlayDate").isTextual shouldBe true
|
||||
}
|
||||
|
||||
"ONGEKI export JSON can be imported without changing data" {
|
||||
assertJsonRoundTrip(OngekiDataExport())
|
||||
}
|
||||
})
|
||||
|
||||
private inline fun <reified T : Any> assertJsonRoundTrip(value: T) {
|
||||
val exportedJson = JACKSON.writeValueAsString(value)
|
||||
val imported = exportedJson.parseJackson(T::class.java)
|
||||
JACKSON.readTree(JACKSON.writeValueAsString(imported)) shouldBe JACKSON.readTree(exportedJson)
|
||||
}
|
||||
Reference in New Issue
Block a user