Skip to content

Commit

Permalink
Merge pull request #36 from spaceapi-community/timestamps
Browse files Browse the repository at this point in the history
Serialize and deserialize timestamp fields as `Instant`
  • Loading branch information
dbrgn authored Sep 2, 2023
2 parents e566ac3 + 4cb76ad commit 82ded77
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions src/main/kotlin/io/spaceapi/types/Event.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@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(
Expand All @@ -31,7 +32,7 @@ data class Event(
@JvmField
var type: String,
@JvmField
var timestamp: Long,
var timestamp: Instant,
@JvmField
var extra: String? = null,
)
7 changes: 4 additions & 3 deletions src/main/kotlin/io/spaceapi/types/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,23 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@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
Expand Down
19 changes: 15 additions & 4 deletions src/main/kotlin/io/spaceapi/types/serializers/Serializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,25 @@ 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
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<URL> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URL", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: URL) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): URL = URL(decoder.decodeString())
}

@ExperimentalSerializationApi
@Serializer(forClass = URI::class)
object URISerializer : KSerializer<URI> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: URI) = encoder.encodeString(value.toString())
Expand All @@ -51,9 +50,21 @@ object URISerializer : KSerializer<URI> {
* by rounding to the closest integer.
*/
@ExperimentalSerializationApi
@Serializer(forClass = Long::class)
object RoundingLongSerializer : KSerializer<Long> {
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<Instant> {
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())
}
41 changes: 32 additions & 9 deletions src/test/kotlin/io/spaceapi/ParserTestKotlin.kt
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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("[email protected]", parsed.contact.email)
assertEquals("irc://freenode.net/#coredump", parsed.contact.irc)
Expand Down Expand Up @@ -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("[email protected]", parsed.contact.email)
assertEquals("irc://freenode.net/#coredump", parsed.contact.irc)
Expand Down Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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)
}
}

0 comments on commit 82ded77

Please sign in to comment.