diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1e0ad..313a7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Possible log types: ### Unreleased +- [changed] Serialize and deserialize timestamp fields as `Instant` (#36) +- [changed] If you're using this library on Android, this requires at least API + level 26 (Android 8), or a backport of the `java.time` APIs! (#36) + ### v0.4.1 (2023-04-25) - [fixed] Make `spacefed.spacephone` a non-required field (#34) diff --git a/src/main/kotlin/io/spaceapi/types/Event.kt b/src/main/kotlin/io/spaceapi/types/Event.kt index 3abb36a..36ab49a 100644 --- a/src/main/kotlin/io/spaceapi/types/Event.kt +++ b/src/main/kotlin/io/spaceapi/types/Event.kt @@ -16,13 +16,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:UseSerializers(RoundingLongSerializer::class) +@file:UseSerializers(TimestampSerializer::class) package io.spaceapi.types -import io.spaceapi.types.serializers.RoundingLongSerializer +import io.spaceapi.types.serializers.TimestampSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import java.time.Instant @Serializable data class Event( @@ -31,7 +32,7 @@ data class Event( @JvmField var type: String, @JvmField - var timestamp: Long, + var timestamp: Instant, @JvmField var extra: String? = null, ) diff --git a/src/main/kotlin/io/spaceapi/types/State.kt b/src/main/kotlin/io/spaceapi/types/State.kt index 874ee67..86afa6e 100644 --- a/src/main/kotlin/io/spaceapi/types/State.kt +++ b/src/main/kotlin/io/spaceapi/types/State.kt @@ -16,22 +16,23 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -@file:UseSerializers(URLSerializer::class, RoundingLongSerializer::class) +@file:UseSerializers(URLSerializer::class, TimestampSerializer::class) package io.spaceapi.types -import io.spaceapi.types.serializers.RoundingLongSerializer +import io.spaceapi.types.serializers.TimestampSerializer import io.spaceapi.types.serializers.URLSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import java.net.URL +import java.time.Instant @Serializable data class State( @JvmField var open: Boolean? = null, @JvmField - var lastchange: Long? = null, + var lastchange: Instant? = null, @JvmField var trigger_person: String? = null, @JvmField diff --git a/src/main/kotlin/io/spaceapi/types/serializers/Serializers.kt b/src/main/kotlin/io/spaceapi/types/serializers/Serializers.kt index 58e3453..fcf91e7 100644 --- a/src/main/kotlin/io/spaceapi/types/serializers/Serializers.kt +++ b/src/main/kotlin/io/spaceapi/types/serializers/Serializers.kt @@ -20,7 +20,6 @@ package io.spaceapi.types.serializers import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor @@ -28,10 +27,11 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import java.net.URI import java.net.URL +import java.time.Instant +import java.util.Date import kotlin.math.round @ExperimentalSerializationApi -@Serializer(forClass = URL::class) object URLSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URL", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: URL) = encoder.encodeString(value.toString()) @@ -39,7 +39,6 @@ object URLSerializer : KSerializer { } @ExperimentalSerializationApi -@Serializer(forClass = URI::class) object URISerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: URI) = encoder.encodeString(value.toString()) @@ -51,9 +50,21 @@ object URISerializer : KSerializer { * by rounding to the closest integer. */ @ExperimentalSerializationApi -@Serializer(forClass = Long::class) object RoundingLongSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("RoundingLong", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Long) = encoder.encodeLong(value) override fun deserialize(decoder: Decoder): Long = round(decoder.decodeDouble()).toLong() } + +/** + * Serialize and deserialize Unix timestamps (in seconds). + * + * Deserializing numbers with fractional seconds is supported, + * but the fractional part is ignored. + */ +@ExperimentalSerializationApi +object TimestampSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Timestamp", PrimitiveKind.DOUBLE) + override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.epochSecond) + override fun deserialize(decoder: Decoder): Instant = Instant.ofEpochSecond(round(decoder.decodeDouble()).toLong()) +} diff --git a/src/test/kotlin/io/spaceapi/ParserTestKotlin.kt b/src/test/kotlin/io/spaceapi/ParserTestKotlin.kt index d1260bf..f9f410d 100644 --- a/src/test/kotlin/io/spaceapi/ParserTestKotlin.kt +++ b/src/test/kotlin/io/spaceapi/ParserTestKotlin.kt @@ -1,14 +1,17 @@ package io.spaceapi import io.spaceapi.types.Contact +import io.spaceapi.types.MemberCount import io.spaceapi.types.SpaceFed import io.spaceapi.types.State +import io.spaceapi.types.serializers.TimestampSerializer import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import org.junit.Assert import java.net.URL +import java.time.Instant import kotlin.test.Test import kotlin.test.assertEquals @@ -52,7 +55,6 @@ class ParserTestKotlin { assertEquals(false, parsed.state!!.open) assertEquals(null, parsed.state!!.lastchange) assertEquals("Open Mondays from 20:00", parsed.state!!.message) - assertEquals("Open Mondays from 20:00", parsed.state!!.message) assertEquals("vorstand@lists.coredump.ch", parsed.contact.email) assertEquals("irc://freenode.net/#coredump", parsed.contact.irc) @@ -103,7 +105,6 @@ class ParserTestKotlin { assertEquals(false, parsed.state!!.open) assertEquals(null, parsed.state!!.lastchange) assertEquals("Open Mondays from 20:00", parsed.state!!.message) - assertEquals("Open Mondays from 20:00", parsed.state!!.message) assertEquals("vorstand@lists.coredump.ch", parsed.contact.email) assertEquals("irc://freenode.net/#coredump", parsed.contact.irc) @@ -156,14 +157,10 @@ class ParserTestKotlin { */ @Test fun parseFloatAsInteger() { - val parsed: State = Json.decodeFromString("""{ - "open": false, - "message": "Open Mondays from 20:00", - "lastchange": 1605400210.0 + val parsed: MemberCount = Json.decodeFromString("""{ + "value": 42.0 }""") - assertEquals(false, parsed.open) - assertEquals("Open Mondays from 20:00", parsed.message) - assertEquals(1605400210L, parsed.lastchange) + assertEquals(42L, parsed.value) } /** @@ -275,4 +272,30 @@ class ParserTestKotlin { @Suppress("DEPRECATION") assertEquals(false, parsed.spacephone) } + + @Test + fun parseTimestampsAsInstant() { + val parsed: State = Json.decodeFromString("""{ + "open": true, + "lastchange": 1693685542 + }""") + assertEquals(true, parsed.open) + assertEquals(Instant.ofEpochSecond(1693685542), parsed.lastchange) + } + + @Test + fun parseFloatTimestampsAsInstant() { + val parsed: State = Json.decodeFromString("""{ + "open": true, + "lastchange": 1693685542.1234321 + }""") + assertEquals(true, parsed.open) + assertEquals(Instant.ofEpochMilli(1693685542000), parsed.lastchange) + } + + @Test + fun serializeInstantsAsLong() { + val serialized = Json.encodeToString(TimestampSerializer, Instant.ofEpochSecond(1693685542)) + assertEquals("1693685542", serialized) + } }