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)
+ }
}