Skip to content

kotlinx-serialization support #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3b65a0e
Implement serialization of an Instant
dkhalanskyjb Nov 20, 2020
357b151
Implement some serializers in common code
dkhalanskyjb Nov 27, 2020
73312a1
Add rough measurements of serialization efficiency
dkhalanskyjb Nov 27, 2020
1881a0d
Add serializers for LocalDate, LocalDateTime, DateTimePeriod
dkhalanskyjb Dec 1, 2020
820516c
Add serializers for DateTimeUnit
dkhalanskyjb Dec 1, 2020
c27e81e
Add error handling to serializers
dkhalanskyjb Dec 1, 2020
ebdf6bf
Serialize time zones
dkhalanskyjb Dec 2, 2020
2eaac88
Rename serializers
dkhalanskyjb Dec 4, 2020
0452296
Implement handwritten serializers for DateTimeUnit
dkhalanskyjb Dec 9, 2020
038ad25
Add tests for serialization
dkhalanskyjb Dec 9, 2020
1459c5c
ISO8601 serializer for DateTimePeriod
dkhalanskyjb Dec 10, 2020
d486f9a
Fix serial descriptor names
dkhalanskyjb Dec 10, 2020
0518207
Only depend on serialization with compileOnly from `core`
dkhalanskyjb Mar 1, 2021
6acf818
Move serializers to a separate package
dkhalanskyjb Mar 10, 2021
756af5c
Cleanup
dkhalanskyjb Mar 11, 2021
2c35bb4
Fixes
dkhalanskyjb Mar 12, 2021
af231f7
Remove the serializers that were decided against
dkhalanskyjb Mar 12, 2021
d2cb1eb
Disable publishing for the serialization subproject
dkhalanskyjb Mar 24, 2021
c5c8ad2
Rename DateTimeUnit serializers
dkhalanskyjb Mar 24, 2021
9ff7e03
Test which serializers are the default ones
dkhalanskyjb Mar 24, 2021
b3ce0fe
Add a (failing) test for contextual serializers usage
dkhalanskyjb Mar 24, 2021
31d8fa3
Fix the failing test and add a test for default serializers
dkhalanskyjb Mar 25, 2021
8170830
ISO -> Iso in serializer names
dkhalanskyjb Mar 26, 2021
b4b29da
Add a test for manual setting of serializers
dkhalanskyjb Mar 26, 2021
f15d28c
Remove unrelated changes to formatting
dkhalanskyjb Mar 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30")
}
}

plugins {
id("kotlinx.team.infra") version "0.3.0-dev-64"
kotlin("plugin.serialization") version "1.4.30"
}

infra {
Expand Down
3 changes: 3 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import javax.xml.parsers.DocumentBuilderFactory

plugins {
id("kotlin-multiplatform")
kotlin("plugin.serialization")
`maven-publish`
}

Expand All @@ -18,6 +19,7 @@ base {

//val JDK_6: String by project
val JDK_8: String by project
val serializationVersion: String by project

kotlin {
infra {
Expand Down Expand Up @@ -148,6 +150,7 @@ kotlin {
commonMain {
dependencies {
api("org.jetbrains.kotlin:kotlin-stdlib-common")
compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion")
}
}

Expand Down
9 changes: 7 additions & 2 deletions core/common/src/DateTimePeriod.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@

package kotlinx.datetime

import kotlinx.datetime.serializers.DatePeriodISO8601Serializer
import kotlinx.datetime.serializers.DateTimePeriodISO8601Serializer
import kotlin.math.*
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlinx.serialization.Serializable

@Serializable(with = DateTimePeriodISO8601Serializer::class)
// TODO: could be error-prone without explicitly named params
sealed class DateTimePeriod {
internal abstract val totalMonths: Int
Expand Down Expand Up @@ -233,9 +237,10 @@ sealed class DateTimePeriod {

public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this)

@Serializable(with = DatePeriodISO8601Serializer::class)
class DatePeriod internal constructor(
internal override val totalMonths: Int,
override val days: Int,
internal override val totalMonths: Int,
override val days: Int,
) : DateTimePeriod() {
constructor(years: Int = 0, months: Int = 0, days: Int = 0): this(totalMonths(years, months), days)
// avoiding excessive computations
Expand Down
14 changes: 10 additions & 4 deletions core/common/src/DateTimeUnit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

package kotlinx.datetime

import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.nanoseconds
import kotlinx.datetime.serializers.*
import kotlinx.serialization.Serializable
import kotlin.time.*

@Serializable(with = DateTimeUnitSerializer::class)
sealed class DateTimeUnit {

abstract operator fun times(scalar: Int): DateTimeUnit

@Serializable(with = TimeBasedDateTimeUnitSerializer::class)
class TimeBased(val nanoseconds: Long) : DateTimeUnit() {
private val unitName: String
private val unitScale: Long
Expand Down Expand Up @@ -51,7 +53,8 @@ sealed class DateTimeUnit {
override fun times(scalar: Int): TimeBased = TimeBased(safeMultiply(nanoseconds, scalar.toLong()))

@ExperimentalTime
val duration: Duration = nanoseconds.nanoseconds
val duration: Duration
get() = nanoseconds.nanoseconds

override fun equals(other: Any?): Boolean =
this === other || (other is TimeBased && this.nanoseconds == other.nanoseconds)
Expand All @@ -61,8 +64,10 @@ sealed class DateTimeUnit {
override fun toString(): String = formatToString(unitScale, unitName)
}

@Serializable(with = DateBasedDateTimeUnitSerializer::class)
sealed class DateBased : DateTimeUnit() {
// TODO: investigate how to move subclasses up to DateTimeUnit scope
@Serializable(with = DayBasedDateTimeUnitSerializer::class)
class DayBased(val days: Int) : DateBased() {
init {
require(days > 0) { "Unit duration must be positive, but was $days days." }
Expand All @@ -80,6 +85,7 @@ sealed class DateTimeUnit {
else
formatToString(days, "DAY")
}
@Serializable(with = MonthBasedDateTimeUnitSerializer::class)
class MonthBased(val months: Int) : DateBased() {
init {
require(months > 0) { "Unit duration must be positive, but was $months months." }
Expand Down
6 changes: 4 additions & 2 deletions core/common/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@

package kotlinx.datetime

import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlinx.datetime.serializers.InstantISO8601Serializer
import kotlinx.serialization.Serializable
import kotlin.time.*

@OptIn(ExperimentalTime::class)
@Serializable(with = InstantISO8601Serializer::class)
public expect class Instant : Comparable<Instant> {

/**
Expand Down
4 changes: 4 additions & 0 deletions core/common/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

package kotlinx.datetime

import kotlinx.datetime.serializers.LocalDateISO8601Serializer
import kotlinx.serialization.Serializable

@Serializable(with = LocalDateISO8601Serializer::class)
public expect class LocalDate : Comparable<LocalDate> {
companion object {
/**
Expand Down
4 changes: 3 additions & 1 deletion core/common/src/LocalDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

package kotlinx.datetime

import kotlinx.datetime.serializers.LocalDateTimeISO8601Serializer
import kotlinx.serialization.Serializable


@Serializable(with = LocalDateTimeISO8601Serializer::class)
public expect class LocalDateTime : Comparable<LocalDateTime> {
companion object {

Expand Down
1 change: 0 additions & 1 deletion core/common/src/Month.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public expect enum class Month {
OCTOBER,
NOVEMBER,
DECEMBER;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: unrelated change

// val value: Int // member missing in java.time.Month has to be an extension
}

Expand Down
6 changes: 6 additions & 0 deletions core/common/src/TimeZone.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

package kotlinx.datetime

import kotlinx.datetime.serializers.TimeZoneSerializer
import kotlinx.datetime.serializers.ZoneOffsetSerializer
import kotlinx.serialization.Serializable

@Serializable(with = TimeZoneSerializer::class)
public expect open class TimeZone {
/**
* Returns the identifier string of the time zone.
Expand Down Expand Up @@ -80,6 +85,7 @@ public expect open class TimeZone {
public fun LocalDateTime.toInstant(): Instant
}

@Serializable(with = ZoneOffsetSerializer::class)
public expect class ZoneOffset : TimeZone {
val totalSeconds: Int
}
Expand Down
3 changes: 3 additions & 0 deletions core/common/src/math.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,12 @@ internal class DivRemResult(val q: Long, val r: Long) {
operator fun component2(): Long = r
}

@Suppress("NOTHING_TO_INLINE")
private inline fun low(x: Long) = x and 0xffffffff
@Suppress("NOTHING_TO_INLINE")
private inline fun high(x: Long) = (x shr 32) and 0xffffffff
/** For [bit] in [0; 63], return bit #[bit] of [value], counting from the least significant bit */
@Suppress("NOTHING_TO_INLINE")
private inline fun indexBit(value: Long, bit: Int): Long = (value shr bit and 1)


Expand Down
153 changes: 153 additions & 0 deletions core/common/src/serializers/DateTimePeriodSerializers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2019-2021 JetBrains s.r.o.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime.serializers

import kotlinx.datetime.DatePeriod
import kotlinx.datetime.DateTimePeriod
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

object DateTimePeriodComponentSerializer: KSerializer<DateTimePeriod> {

override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("DateTimePeriod") {
element<Int>("years", isOptional = true)
element<Int>("months", isOptional = true)
element<Int>("days", isOptional = true)
element<Int>("hours", isOptional = true)
element<Int>("minutes", isOptional = true)
element<Int>("seconds", isOptional = true)
element<Long>("nanoseconds", isOptional = true)
}

override fun deserialize(decoder: Decoder): DateTimePeriod =
decoder.decodeStructure(descriptor) {
var years = 0
var months = 0
var days = 0
var hours = 0
var minutes = 0
var seconds = 0
var nanoseconds = 0L
loop@while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> years = decodeIntElement(descriptor, 0)
1 -> months = decodeIntElement(descriptor, 1)
2 -> days = decodeIntElement(descriptor, 2)
3 -> hours = decodeIntElement(descriptor, 3)
4 -> minutes = decodeIntElement(descriptor, 4)
5 -> seconds = decodeIntElement(descriptor, 5)
6 -> nanoseconds = decodeLongElement(descriptor, 6)
CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262
else -> throw SerializationException("Unexpected index: $index")
}
}
DateTimePeriod(years, months, days, hours, minutes, seconds, nanoseconds)
}

override fun serialize(encoder: Encoder, value: DateTimePeriod) {
encoder.encodeStructure(descriptor) {
with(value) {
if (years != 0) encodeIntElement(descriptor, 0, years)
if (months != 0) encodeIntElement(descriptor, 1, months)
if (days != 0) encodeIntElement(descriptor, 2, days)
if (hours != 0) encodeIntElement(descriptor, 3, hours)
if (minutes != 0) encodeIntElement(descriptor, 4, minutes)
if (seconds != 0) encodeIntElement(descriptor, 5, seconds)
if (nanoseconds != 0) encodeLongElement(descriptor, 6, value.nanoseconds.toLong())
}
}
}

}

object DateTimePeriodISO8601Serializer: KSerializer<DateTimePeriod> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("DateTimePeriod", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): DateTimePeriod =
DateTimePeriod.parse(decoder.decodeString())

override fun serialize(encoder: Encoder, value: DateTimePeriod) {
encoder.encodeString(value.toString())
}

}

object DatePeriodComponentSerializer: KSerializer<DatePeriod> {

private fun unexpectedNonzero(fieldName: String, value: Long) {
if (value != 0L) {
throw SerializationException("DatePeriod should have non-date components be zero, but got $value in '$fieldName'")
}
}

private fun unexpectedNonzero(fieldName: String, value: Int) = unexpectedNonzero(fieldName, value.toLong())

override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("DatePeriod") {
element<Int>("years", isOptional = true)
element<Int>("months", isOptional = true)
element<Int>("days", isOptional = true)
element<Int>("hours", isOptional = true)
element<Int>("minutes", isOptional = true)
element<Int>("seconds", isOptional = true)
element<Long>("nanoseconds", isOptional = true)
}

override fun deserialize(decoder: Decoder): DatePeriod =
decoder.decodeStructure(descriptor) {
var years = 0
var months = 0
var days = 0
loop@while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> years = decodeIntElement(descriptor, 0)
1 -> months = decodeIntElement(descriptor, 1)
2 -> days = decodeIntElement(descriptor, 2)
3 -> unexpectedNonzero("hours", decodeIntElement(descriptor, 3))
4 -> unexpectedNonzero("minutes", decodeIntElement(descriptor, 4))
5 -> unexpectedNonzero("seconds", decodeIntElement(descriptor, 5))
6 -> unexpectedNonzero("nanoseconds", decodeLongElement(descriptor, 6))
CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262
else -> throw SerializationException("Unexpected index: $index")
}
}
DatePeriod(years, months, days)
}

override fun serialize(encoder: Encoder, value: DatePeriod) {
encoder.encodeStructure(descriptor) {
with(value) {
if (years != 0) encodeIntElement(DateTimePeriodComponentSerializer.descriptor, 0, years)
if (months != 0) encodeIntElement(DateTimePeriodComponentSerializer.descriptor, 1, months)
if (days != 0) encodeIntElement(DateTimePeriodComponentSerializer.descriptor, 2, days)
}
}
}

}

object DatePeriodISO8601Serializer: KSerializer<DatePeriod> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you all use as a style guide, but I try really hard to evangelize the practice of treating acronyms and acronym-like things such as ISO, IPv6, iOS, etc. as words and casing them as such.

Google's Java and Kotlin style guide recommend this practice:

That would make this type be DatePeriodIso8601Serializer. And I think there's one or two more in the change.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for DatePeriodIso8601Serializer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, it would be consistent with Duration.toIsoString in stdlib, for example.

What do you think about dropping 8601 suffix? On one hand, it's not that there are many ISO standards on date representation, on the other hand, the serializer name is not supposed to be used often, so the character economy there may be not worth it.


override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("DatePeriod", PrimitiveKind.STRING)

// TODO: consider whether should fail when parsing "P1YT0H0M0.0S"
override fun deserialize(decoder: Decoder): DatePeriod =
when (val period = DateTimePeriod.parse(decoder.decodeString())) {
is DatePeriod -> period
else -> throw SerializationException("$period is not a date-based period")
}

override fun serialize(encoder: Encoder, value: DatePeriod) {
encoder.encodeString(value.toString())
}

}
Loading