diff --git a/build.gradle.kts b/build.gradle.kts index 57157244a..441973702 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 2c923cd8f..606d9e28c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -5,6 +5,7 @@ import javax.xml.parsers.DocumentBuilderFactory plugins { id("kotlin-multiplatform") + kotlin("plugin.serialization") `maven-publish` } @@ -18,6 +19,7 @@ base { //val JDK_6: String by project val JDK_8: String by project +val serializationVersion: String by project kotlin { infra { @@ -148,6 +150,7 @@ kotlin { commonMain { dependencies { api("org.jetbrains.kotlin:kotlin-stdlib-common") + compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion") } } diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 8d454ce97..1d76d19a3 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -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 @@ -233,6 +237,7 @@ 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, diff --git a/core/common/src/DateTimeUnit.kt b/core/common/src/DateTimeUnit.kt index 68a420915..06a851ed8 100644 --- a/core/common/src/DateTimeUnit.kt +++ b/core/common/src/DateTimeUnit.kt @@ -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 @@ -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) @@ -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." } @@ -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." } diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index d414fbac9..01bfaa0d8 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -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 { /** diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 43045a15e..40c89ec7f 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -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 { companion object { /** diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index 9b8e84435..773710262 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -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 { companion object { diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index e0dfc0e56..11e0d4144 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -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. @@ -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 } diff --git a/core/common/src/math.kt b/core/common/src/math.kt index c164f2ddd..601040e30 100644 --- a/core/common/src/math.kt +++ b/core/common/src/math.kt @@ -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) diff --git a/core/common/src/serializers/DateTimePeriodSerializers.kt b/core/common/src/serializers/DateTimePeriodSerializers.kt new file mode 100644 index 000000000..48efebf00 --- /dev/null +++ b/core/common/src/serializers/DateTimePeriodSerializers.kt @@ -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 { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("DateTimePeriod") { + element("years", isOptional = true) + element("months", isOptional = true) + element("days", isOptional = true) + element("hours", isOptional = true) + element("minutes", isOptional = true) + element("seconds", isOptional = true) + element("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 { + + 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 { + + 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("years", isOptional = true) + element("months", isOptional = true) + element("days", isOptional = true) + element("hours", isOptional = true) + element("minutes", isOptional = true) + element("seconds", isOptional = true) + element("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 { + + 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()) + } + +} \ No newline at end of file diff --git a/core/common/src/serializers/DateTimeUnitSerializers.kt b/core/common/src/serializers/DateTimeUnitSerializers.kt new file mode 100644 index 000000000..449402691 --- /dev/null +++ b/core/common/src/serializers/DateTimeUnitSerializers.kt @@ -0,0 +1,186 @@ +/* + * 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.DateTimeUnit +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.* +import kotlinx.serialization.internal.AbstractPolymorphicSerializer +import kotlin.reflect.KClass + +object TimeBasedDateTimeUnitSerializer: KSerializer { + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("TimeBased") { + element("nanoseconds") + } + + override fun serialize(encoder: Encoder, value: DateTimeUnit.TimeBased) { + encoder.encodeStructure(descriptor) { + encodeLongElement(descriptor, 0, value.nanoseconds) + } + } + + @ExperimentalSerializationApi + @Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException` + override fun deserialize(decoder: Decoder): DateTimeUnit.TimeBased { + var seen = false + var nanoseconds = 0L + decoder.decodeStructure(descriptor) { + if (decodeSequentially()) { + nanoseconds = decodeLongElement(descriptor, 0) + seen = true + } else { + loop@while (true) { + when (val elementIndex: Int = decodeElementIndex(descriptor)) { + 0 -> { + nanoseconds = decodeLongElement(descriptor, 0) + seen = true + } + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw UnknownFieldException(elementIndex) + } + } + } + } + if (!seen) throw MissingFieldException("nanoseconds") + return DateTimeUnit.TimeBased(nanoseconds) + } +} + +object DayBasedDateTimeUnitSerializer: KSerializer { + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DayBased") { + element("days") + } + + override fun serialize(encoder: Encoder, value: DateTimeUnit.DateBased.DayBased) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.days) + } + } + + @ExperimentalSerializationApi + @Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException` + override fun deserialize(decoder: Decoder): DateTimeUnit.DateBased.DayBased { + var seen = false + var days = 0 + decoder.decodeStructure(descriptor) { + if (decodeSequentially()) { + days = decodeIntElement(descriptor, 0) + seen = true + } else { + loop@while (true) { + when (val elementIndex: Int = decodeElementIndex(descriptor)) { + 0 -> { + days = decodeIntElement(descriptor, 0) + seen = true + } + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw UnknownFieldException(elementIndex) + } + } + } + } + if (!seen) throw MissingFieldException("days") + return DateTimeUnit.DateBased.DayBased(days) + } +} + +object MonthBasedDateTimeUnitSerializer: KSerializer { + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MonthBased") { + element("months") + } + + override fun serialize(encoder: Encoder, value: DateTimeUnit.DateBased.MonthBased) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.months) + } + } + + @ExperimentalSerializationApi + @Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException` + override fun deserialize(decoder: Decoder): DateTimeUnit.DateBased.MonthBased { + var seen = false + var months = 0 + decoder.decodeStructure(descriptor) { + if (decodeSequentially()) { + months = decodeIntElement(descriptor, 0) + seen = true + } else { + loop@while (true) { + when (val elementIndex: Int = decodeElementIndex(descriptor)) { + 0 -> { + months = decodeIntElement(descriptor, 0) + seen = true + } + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw UnknownFieldException(elementIndex) + } + } + } + } + if (!seen) throw MissingFieldException("months") + return DateTimeUnit.DateBased.MonthBased(months) + } +} + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR", "INVISIBLE_MEMBER") +object DateBasedDateTimeUnitSerializer: AbstractPolymorphicSerializer() { + + private val impl = SealedClassSerializer("kotlinx.datetime.DateTimeUnit.DateBased", + DateTimeUnit.DateBased::class, + arrayOf(DateTimeUnit.DateBased.DayBased::class, DateTimeUnit.DateBased.MonthBased::class), + arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer)) + + @InternalSerializationApi + override fun findPolymorphicSerializerOrNull(decoder: CompositeDecoder, klassName: String?): + DeserializationStrategy? = + impl.findPolymorphicSerializerOrNull(decoder, klassName) + + @InternalSerializationApi + override fun findPolymorphicSerializerOrNull(encoder: Encoder, value: DateTimeUnit.DateBased): + SerializationStrategy? = + impl.findPolymorphicSerializerOrNull(encoder, value) + + @InternalSerializationApi + override val baseClass: KClass + get() = DateTimeUnit.DateBased::class + + @InternalSerializationApi + override val descriptor: SerialDescriptor + get() = impl.descriptor + +} + +@Suppress("EXPERIMENTAL_API_USAGE_ERROR", "INVISIBLE_MEMBER") +object DateTimeUnitSerializer: AbstractPolymorphicSerializer() { + + private val impl = SealedClassSerializer("kotlinx.datetime.DateTimeUnit", + DateTimeUnit::class, + arrayOf(DateTimeUnit.DateBased.DayBased::class, DateTimeUnit.DateBased.MonthBased::class, DateTimeUnit.TimeBased::class), + arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer, TimeBasedDateTimeUnitSerializer)) + + @InternalSerializationApi + override fun findPolymorphicSerializerOrNull(decoder: CompositeDecoder, klassName: String?): DeserializationStrategy? = + impl.findPolymorphicSerializerOrNull(decoder, klassName) + + @InternalSerializationApi + override fun findPolymorphicSerializerOrNull(encoder: Encoder, value: DateTimeUnit): SerializationStrategy? = + impl.findPolymorphicSerializerOrNull(encoder, value) + + @InternalSerializationApi + override val baseClass: KClass + get() = DateTimeUnit::class + + @InternalSerializationApi + override val descriptor: SerialDescriptor + get() = impl.descriptor + +} diff --git a/core/common/src/serializers/DayOfWeekSerializers.kt b/core/common/src/serializers/DayOfWeekSerializers.kt new file mode 100644 index 000000000..b1c13183b --- /dev/null +++ b/core/common/src/serializers/DayOfWeekSerializers.kt @@ -0,0 +1,24 @@ +/* + * 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.DayOfWeek +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.internal.* + +@Suppress("INVISIBLE_MEMBER") +object DayOfWeekSerializer: KSerializer { + private val impl = EnumSerializer("Month", DayOfWeek.values()) + + override val descriptor: SerialDescriptor + get() = impl.descriptor + + override fun deserialize(decoder: Decoder): DayOfWeek = impl.deserialize(decoder) + + override fun serialize(encoder: Encoder, value: DayOfWeek) = impl.serialize(encoder, value) +} diff --git a/core/common/src/serializers/InstantSerializers.kt b/core/common/src/serializers/InstantSerializers.kt new file mode 100644 index 000000000..3464353ff --- /dev/null +++ b/core/common/src/serializers/InstantSerializers.kt @@ -0,0 +1,61 @@ +/* + * 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.Instant +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +object InstantIso8601Serializer: KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Instant = + Instant.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } + +} + +object InstantComponentSerializer: KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Instant") { + element("epochSeconds") + element("nanosecondsOfSecond", isOptional = true) + } + + @Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException` + override fun deserialize(decoder: Decoder): Instant = + decoder.decodeStructure(descriptor) { + var epochSeconds: Long? = null + var nanosecondsOfSecond = 0 + loop@while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> epochSeconds = decodeLongElement(descriptor, 0) + 1 -> nanosecondsOfSecond = decodeIntElement(descriptor, 1) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw SerializationException("Unexpected index: $index") + } + } + if (epochSeconds == null) throw MissingFieldException("epochSeconds") + Instant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond) + } + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeStructure(descriptor) { + encodeLongElement(descriptor, 0, value.epochSeconds) + if (value.nanosecondsOfSecond != 0) { + encodeIntElement(descriptor, 1, value.nanosecondsOfSecond) + } + } + } + +} diff --git a/core/common/src/serializers/LocalDateSerializers.kt b/core/common/src/serializers/LocalDateSerializers.kt new file mode 100644 index 000000000..39d6fedee --- /dev/null +++ b/core/common/src/serializers/LocalDateSerializers.kt @@ -0,0 +1,65 @@ +/* + * 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.LocalDate +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +object LocalDateIso8601Serializer: KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): LocalDate = + LocalDate.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: LocalDate) { + encoder.encodeString(value.toString()) + } + +} + +object LocalDateComponentSerializer: KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("LocalDate") { + element("year") + element("month") + element("day") + } + + @Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException` + override fun deserialize(decoder: Decoder): LocalDate = + decoder.decodeStructure(descriptor) { + var year: Int? = null + var month: Short? = null + var day: Short? = null + loop@while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> year = decodeIntElement(descriptor, 0) + 1 -> month = decodeShortElement(descriptor, 1) + 2 -> day = decodeShortElement(descriptor, 2) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw SerializationException("Unexpected index: $index") + } + } + if (year == null) throw MissingFieldException("year") + if (month == null) throw MissingFieldException("month") + if (day == null) throw MissingFieldException("day") + LocalDate(year, month.toInt(), day.toInt()) + } + + override fun serialize(encoder: Encoder, value: LocalDate) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.year) + encodeShortElement(descriptor, 1, value.monthNumber.toShort()) + encodeShortElement(descriptor, 2, value.dayOfMonth.toShort()) + } + } + +} \ No newline at end of file diff --git a/core/common/src/serializers/LocalDateTimeSerializers.kt b/core/common/src/serializers/LocalDateTimeSerializers.kt new file mode 100644 index 000000000..aa2576ca5 --- /dev/null +++ b/core/common/src/serializers/LocalDateTimeSerializers.kt @@ -0,0 +1,87 @@ +/* + * 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.LocalDateTime +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +object LocalDateTimeIso8601Serializer: KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): LocalDateTime = + LocalDateTime.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: LocalDateTime) { + encoder.encodeString(value.toString()) + } + +} + +object LocalDateTimeComponentSerializer: KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("LocalDateTime") { + element("year") + element("month") + element("day") + element("hour") + element("minute") + element("second", isOptional = true) + element("nanosecond", isOptional = true) + } + + @Suppress("INVISIBLE_MEMBER") // to be able to throw `MissingFieldException` + override fun deserialize(decoder: Decoder): LocalDateTime = + decoder.decodeStructure(descriptor) { + var year: Int? = null + var month: Short? = null + var day: Short? = null + var hour: Short? = null + var minute: Short? = null + var second: Short = 0 + var nanosecond = 0 + loop@while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> year = decodeIntElement(descriptor, 0) + 1 -> month = decodeShortElement(descriptor, 1) + 2 -> day = decodeShortElement(descriptor, 2) + 3 -> hour = decodeShortElement(descriptor, 3) + 4 -> minute = decodeShortElement(descriptor, 4) + 5 -> second = decodeShortElement(descriptor, 5) + 6 -> nanosecond = decodeIntElement(descriptor, 6) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw SerializationException("Unexpected index: $index") + } + } + if (year == null) throw MissingFieldException("year") + if (month == null) throw MissingFieldException("month") + if (day == null) throw MissingFieldException("day") + if (hour == null) throw MissingFieldException("hour") + if (minute == null) throw MissingFieldException("minute") + LocalDateTime(year, month.toInt(), day.toInt(), hour.toInt(), minute.toInt(), second.toInt(), nanosecond) + } + + override fun serialize(encoder: Encoder, value: LocalDateTime) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.year) + encodeShortElement(descriptor, 1, value.monthNumber.toShort()) + encodeShortElement(descriptor, 2, value.dayOfMonth.toShort()) + encodeShortElement(descriptor, 3, value.hour.toShort()) + encodeShortElement(descriptor, 4, value.minute.toShort()) + if (value.second != 0 || value.nanosecond != 0) { + encodeShortElement(descriptor, 5, value.second.toShort()) + if (value.nanosecond != 0) { + encodeIntElement(descriptor, 6, value.nanosecond) + } + } + } + } + +} \ No newline at end of file diff --git a/core/common/src/serializers/MonthSerializers.kt b/core/common/src/serializers/MonthSerializers.kt new file mode 100644 index 000000000..d0aaae85a --- /dev/null +++ b/core/common/src/serializers/MonthSerializers.kt @@ -0,0 +1,24 @@ +/* + * 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.Month +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.internal.* + +@Suppress("INVISIBLE_MEMBER") +object MonthSerializer: KSerializer { + private val impl = EnumSerializer("Month", Month.values()) + + override val descriptor: SerialDescriptor + get() = impl.descriptor + + override fun deserialize(decoder: Decoder): Month = impl.deserialize(decoder) + + override fun serialize(encoder: Encoder, value: Month) = impl.serialize(encoder, value) +} diff --git a/core/common/src/serializers/TimeZoneSerializers.kt b/core/common/src/serializers/TimeZoneSerializers.kt new file mode 100644 index 000000000..f34cd1fbc --- /dev/null +++ b/core/common/src/serializers/TimeZoneSerializers.kt @@ -0,0 +1,43 @@ +/* + * 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.TimeZone +import kotlinx.datetime.ZoneOffset +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +object TimeZoneSerializer: KSerializer { + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TimeZone", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): TimeZone = TimeZone.of(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: TimeZone) { + encoder.encodeString(value.id) + } + +} + +object ZoneOffsetSerializer: KSerializer { + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ZoneOffset", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): ZoneOffset { + val zone = TimeZone.of(decoder.decodeString()) + if (zone is ZoneOffset) { + return zone + } else { + throw SerializationException("Timezone identifier '$zone' does not correspond to a fixed-offset timezone") + } + } + + override fun serialize(encoder: Encoder, value: ZoneOffset) { + encoder.encodeString(value.id) + } + +} diff --git a/core/js/src/Instant.kt b/core/js/src/Instant.kt index d371808ea..35f2e0f3c 100644 --- a/core/js/src/Instant.kt +++ b/core/js/src/Instant.kt @@ -14,8 +14,11 @@ import kotlinx.datetime.internal.JSJoda.Instant as jtInstant import kotlinx.datetime.internal.JSJoda.Duration as jtDuration import kotlinx.datetime.internal.JSJoda.Clock as jtClock import kotlinx.datetime.internal.JSJoda.ChronoUnit +import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.serialization.Serializable import kotlin.math.truncate +@Serializable(with = InstantIso8601Serializer::class) @OptIn(ExperimentalTime::class) public actual class Instant internal constructor(internal val value: jtInstant) : Comparable { diff --git a/core/js/src/LocalDate.kt b/core/js/src/LocalDate.kt index b384b86b0..94b5ebe6f 100644 --- a/core/js/src/LocalDate.kt +++ b/core/js/src/LocalDate.kt @@ -6,8 +6,11 @@ package kotlinx.datetime import kotlinx.datetime.internal.JSJoda.ChronoUnit +import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.LocalDate as jtLocalDate +@Serializable(with = LocalDateIso8601Serializer::class) public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { actual companion object { public actual fun parse(isoString: String): LocalDate = try { diff --git a/core/js/src/LocalDateTime.kt b/core/js/src/LocalDateTime.kt index 02366701e..3f99d21f5 100644 --- a/core/js/src/LocalDateTime.kt +++ b/core/js/src/LocalDateTime.kt @@ -4,9 +4,11 @@ */ package kotlinx.datetime +import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer +import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.LocalDateTime as jtLocalDateTime - +@Serializable(with = LocalDateTimeIso8601Serializer::class) public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable { public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : diff --git a/core/js/src/TimeZone.kt b/core/js/src/TimeZone.kt index 1fa0ce556..a37daebca 100644 --- a/core/js/src/TimeZone.kt +++ b/core/js/src/TimeZone.kt @@ -5,8 +5,12 @@ package kotlinx.datetime import kotlinx.datetime.internal.JSJoda.ZoneId +import kotlinx.datetime.serializers.TimeZoneSerializer +import kotlinx.datetime.serializers.ZoneOffsetSerializer +import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.ZoneOffset as jtZoneOffset +@Serializable(with = TimeZoneSerializer::class) actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { public actual val id: String get() = zoneId.id() @@ -27,7 +31,12 @@ actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { actual val UTC: TimeZone = jtZoneOffset.UTC.let(::TimeZone) actual fun of(zoneId: String): TimeZone = try { - ZoneId.of(zoneId).let(::TimeZone) + val zone = ZoneId.of(zoneId) + if (zone is jtZoneOffset) { + ZoneOffset(zone) + } else { + TimeZone(zone) + } } catch (e: Throwable) { if (e.isJodaDateTimeException()) throw IllegalTimeZoneException(e) throw e @@ -37,6 +46,7 @@ actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { } } +@Serializable(with = ZoneOffsetSerializer::class) public actual class ZoneOffset internal constructor(zoneOffset: jtZoneOffset): TimeZone(zoneOffset) { internal val zoneOffset get() = zoneId as jtZoneOffset diff --git a/core/jvm/src/Instant.kt b/core/jvm/src/Instant.kt index 988742bc3..79cf7554d 100644 --- a/core/jvm/src/Instant.kt +++ b/core/jvm/src/Instant.kt @@ -6,6 +6,8 @@ package kotlinx.datetime +import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit @@ -13,6 +15,7 @@ import kotlin.time.* import java.time.Instant as jtInstant import java.time.Clock as jtClock +@Serializable(with = InstantIso8601Serializer::class) @OptIn(ExperimentalTime::class) public actual class Instant internal constructor(internal val value: jtInstant) : Comparable { diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index d76f5f576..d3708ae54 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -5,12 +5,14 @@ @file:JvmName("LocalDateJvmKt") package kotlinx.datetime +import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit import java.time.LocalDate as jtLocalDate - +@Serializable(with = LocalDateIso8601Serializer::class) public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { actual companion object { public actual fun parse(isoString: String): LocalDate = try { diff --git a/core/jvm/src/LocalDateTime.kt b/core/jvm/src/LocalDateTime.kt index 235146b50..4d4e38d3c 100644 --- a/core/jvm/src/LocalDateTime.kt +++ b/core/jvm/src/LocalDateTime.kt @@ -5,14 +5,16 @@ @file:JvmName("LocalDateTimeJvmKt") package kotlinx.datetime +import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer +import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.format.DateTimeParseException import java.time.LocalDateTime as jtLocalDateTime - public actual typealias Month = java.time.Month public actual typealias DayOfWeek = java.time.DayOfWeek +@Serializable(with = LocalDateTimeIso8601Serializer::class) public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable { public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) : diff --git a/core/jvm/src/TimeZoneJvm.kt b/core/jvm/src/TimeZoneJvm.kt index 6435083b7..21eeb0183 100644 --- a/core/jvm/src/TimeZoneJvm.kt +++ b/core/jvm/src/TimeZoneJvm.kt @@ -8,10 +8,14 @@ package kotlinx.datetime +import kotlinx.datetime.serializers.TimeZoneSerializer +import kotlinx.datetime.serializers.ZoneOffsetSerializer +import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.ZoneId import java.time.ZoneOffset as jtZoneOffset +@Serializable(with = TimeZoneSerializer::class) actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { public actual val id: String get() = zoneId.id @@ -32,8 +36,12 @@ actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { actual val UTC: TimeZone = jtZoneOffset.UTC.let(::TimeZone) actual fun of(zoneId: String): TimeZone = try { - // TODO: Return ZoneOffset for j.t.ZoneOffset - ZoneId.of(zoneId).let(::TimeZone) + val zone = ZoneId.of(zoneId) + if (zone is jtZoneOffset) { + ZoneOffset(zone) + } else { + TimeZone(zone) + } } catch (e: Exception) { if (e is DateTimeException) throw IllegalTimeZoneException(e) throw e @@ -43,6 +51,7 @@ actual open class TimeZone internal constructor(internal val zoneId: ZoneId) { } } +@Serializable(with = ZoneOffsetSerializer::class) public actual class ZoneOffset internal constructor(zoneOffset: jtZoneOffset): TimeZone(zoneOffset) { internal val zoneOffset get() = zoneId as jtZoneOffset diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 32b924a31..74f3baac8 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -8,6 +8,8 @@ package kotlinx.datetime +import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.serialization.Serializable import kotlin.math.* import kotlin.time.* @@ -83,6 +85,7 @@ private fun isValidInstantSecond(second: Long) = second >= MIN_SECOND && second internal expect fun currentTime(): Instant +@Serializable(with = InstantIso8601Serializer::class) @OptIn(ExperimentalTime::class) public actual class Instant internal constructor(actual val epochSeconds: Long, actual val nanosecondsOfSecond: Int) : Comparable { diff --git a/core/native/src/LocalDate.kt b/core/native/src/LocalDate.kt index c73446756..e3b13d064 100644 --- a/core/native/src/LocalDate.kt +++ b/core/native/src/LocalDate.kt @@ -8,6 +8,8 @@ package kotlinx.datetime +import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.serialization.Serializable import kotlin.math.* // This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5 @@ -34,6 +36,7 @@ internal const val YEAR_MAX = 999_999 private fun isValidYear(year: Int): Boolean = year >= YEAR_MIN && year <= YEAR_MAX +@Serializable(with = LocalDateIso8601Serializer::class) public actual class LocalDate actual constructor(actual val year: Int, actual val monthNumber: Int, actual val dayOfMonth: Int) : Comparable { init { @@ -63,7 +66,7 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va internal fun ofEpochDay(epochDay: Int): LocalDate { // LocalDate(-999999, 1, 1).toEpochDay(), LocalDate(999999, 12, 31).toEpochDay() // Unidiomatic code due to https://github.com/Kotlin/kotlinx-datetime/issues/5 - require(epochDay >= -365961662 && epochDay <= 364522971) { + require(epochDay >= MIN_EPOCH_DAY && epochDay <= MAX_EPOCH_DAY) { "Invalid date: boundaries of LocalDate exceeded" } var zeroDay = epochDay + DAYS_0000_TO_1970 @@ -97,6 +100,9 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va internal actual val MIN = LocalDate(YEAR_MIN, 1, 1) internal actual val MAX = LocalDate(YEAR_MAX, 12, 31) + + internal const val MIN_EPOCH_DAY = -365961662 + internal const val MAX_EPOCH_DAY = 364522971 } // org.threeten.bp.LocalDate#toEpochDay diff --git a/core/native/src/LocalDateTime.kt b/core/native/src/LocalDateTime.kt index b2677392a..c79a146b5 100644 --- a/core/native/src/LocalDateTime.kt +++ b/core/native/src/LocalDateTime.kt @@ -8,6 +8,9 @@ package kotlinx.datetime +import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer +import kotlinx.serialization.Serializable + // This is a function and not a value due to https://github.com/Kotlin/kotlinx-datetime/issues/5 // org.threeten.bp.format.DateTimeFormatter#ISO_LOCAL_DATE_TIME internal val localDateTimeParser: Parser @@ -18,6 +21,7 @@ internal val localDateTimeParser: Parser LocalDateTime(date, time) } +@Serializable(with = LocalDateTimeIso8601Serializer::class) public actual class LocalDateTime internal constructor( actual val date: LocalDate, internal val time: LocalTime) : Comparable { actual companion object { diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index cb37ada2f..3c81f7118 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -8,9 +8,13 @@ package kotlinx.datetime +import kotlinx.datetime.serializers.TimeZoneSerializer +import kotlinx.datetime.serializers.ZoneOffsetSerializer import kotlin.math.abs import kotlin.native.concurrent.* +import kotlinx.serialization.Serializable +@Serializable(with = TimeZoneSerializer::class) public actual open class TimeZone internal constructor(internal val value: TimeZoneImpl) { actual companion object { @@ -79,6 +83,7 @@ public actual open class TimeZone internal constructor(internal val value: TimeZ @ThreadLocal private var zoneOffsetCache: MutableMap = mutableMapOf() +@Serializable(with = ZoneOffsetSerializer::class) public actual class ZoneOffset internal constructor(internal val offset: ZoneOffsetImpl) : TimeZone(offset) { actual val totalSeconds get() = offset.totalSeconds diff --git a/gradle.properties b/gradle.properties index 7dd4c0c46..c8230af1d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,6 +4,8 @@ group=org.jetbrains.kotlinx version=0.2.0 versionSuffix=SNAPSHOT +serializationVersion=1.1.0 + kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.mpp.enableCompatibilityMetadataVariant=true kotlin.js.compiler=both diff --git a/serialization/build.gradle.kts b/serialization/build.gradle.kts new file mode 100644 index 000000000..432f00a8a --- /dev/null +++ b/serialization/build.gradle.kts @@ -0,0 +1,102 @@ +import java.util.Locale + +plugins { + id("kotlin-multiplatform") + kotlin("plugin.serialization") +} + +val JDK_8: String by project +val serializationVersion: String by project + +kotlin { + infra { + target("linuxX64") + target("mingwX64") + target("macosX64") + target("iosX64") + target("iosArm64") + target("iosArm32") + target("watchosArm32") + target("watchosArm64") + target("watchosX86") + target("tvosArm64") + target("tvosX64") + } + + jvm { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8) + } + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + jdkHome = JDK_8 + } + } + + } + + js { + nodejs { + } + compilations.all { + kotlinOptions { + sourceMap = true + moduleKind = "umd" + metaInfo = true + } + } + } + + sourceSets.all { + val suffixIndex = name.indexOfLast { it.isUpperCase() } + val targetName = name.substring(0, suffixIndex) + val suffix = name.substring(suffixIndex).toLowerCase(Locale.ROOT).takeIf { it != "main" } + kotlin.srcDir("$targetName/${suffix ?: "src"}") + resources.srcDir("$targetName/${suffix?.let { it + "Resources "} ?: "resources"}") + languageSettings.apply { + useExperimentalAnnotation("kotlin.Experimental") + } + } + + targets.withType { + compilations["test"].kotlinOptions { + freeCompilerArgs += listOf("-trw") + } + } + + sourceSets { + commonMain { + dependencies { + api(project(":kotlinx-datetime")) + api("org.jetbrains.kotlin:kotlin-stdlib-common") + } + } + + commonTest { + dependencies { + api("org.jetbrains.kotlin:kotlin-test-common") + api("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") + api("org.jetbrains.kotlin:kotlin-test-annotations-common") + } + } + + val jvmMain by getting + val jvmTest by getting { + dependencies { + api("org.jetbrains.kotlin:kotlin-test-junit") + } + } + + val jsMain by getting + val jsTest by getting { + dependencies { + api("org.jetbrains.kotlin:kotlin-test-js") + implementation(npm("@js-joda/timezone", "2.3.0")) + } + } + + val nativeMain by getting + val nativeTest by getting + } +} diff --git a/serialization/common/test/DateTimePeriodSerializationTest.kt b/serialization/common/test/DateTimePeriodSerializationTest.kt new file mode 100644 index 000000000..b70407e71 --- /dev/null +++ b/serialization/common/test/DateTimePeriodSerializationTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlin.test.* + +class DateTimePeriodSerializationTest { + + private fun datePeriodIso8601Serialization( + datePeriodSerializer: KSerializer, + dateTimePeriodSerializer: KSerializer + ) { + for ((period, json) in listOf( + Pair(DatePeriod(1, 2, 3), "\"P1Y2M3D\""), + Pair(DatePeriod(years = 1), "\"P1Y\""), + Pair(DatePeriod(years = 1, months = 1), "\"P1Y1M\""), + Pair(DatePeriod(months = 11), "\"P11M\""), + Pair(DatePeriod(months = 14), "\"P1Y2M\""), + Pair(DatePeriod(months = 10, days = 5), "\"P10M5D\""), + Pair(DatePeriod(years = 1, days = 40), "\"P1Y40D\""), + )) { + assertEquals(json, Json.encodeToString(datePeriodSerializer, period)) + assertEquals(period, Json.decodeFromString(datePeriodSerializer, json)) + assertEquals(json, Json.encodeToString(dateTimePeriodSerializer, period)) + assertEquals(period, Json.decodeFromString(dateTimePeriodSerializer, json) as DatePeriod) + } + // time-based keys should not be considered unknown here + assertFailsWith { + Json { ignoreUnknownKeys = true }.decodeFromString(datePeriodSerializer, "\"P3DT1H\"") + } + // presence of time-based keys should not be a problem if the values are 0 + Json.decodeFromString(datePeriodSerializer, "\"P3DT0H\"") + } + + private fun datePeriodComponentSerialization( + datePeriodSerializer: KSerializer, + dateTimePeriodSerializer: KSerializer + ) { + for ((period, json) in listOf( + Pair(DatePeriod(1, 2, 3), "{\"years\":1,\"months\":2,\"days\":3}"), + Pair(DatePeriod(years = 1), "{\"years\":1}"), + Pair(DatePeriod(years = 1, months = 1), "{\"years\":1,\"months\":1}"), + Pair(DatePeriod(months = 11), "{\"months\":11}"), + Pair(DatePeriod(months = 14), "{\"years\":1,\"months\":2}"), + Pair(DatePeriod(months = 10, days = 5), "{\"months\":10,\"days\":5}"), + Pair(DatePeriod(years = 1, days = 40), "{\"years\":1,\"days\":40}"), + )) { + assertEquals(json, Json.encodeToString(datePeriodSerializer, period)) + assertEquals(period, Json.decodeFromString(datePeriodSerializer, json)) + assertEquals(json, Json.encodeToString(dateTimePeriodSerializer, period)) + assertEquals(period, Json.decodeFromString(dateTimePeriodSerializer, json) as DatePeriod) + } + // time-based keys should not be considered unknown here + assertFailsWith { + Json { ignoreUnknownKeys = true }.decodeFromString(datePeriodSerializer, "{\"hours\":3}") + } + // presence of time-based keys should not be a problem if the values are 0 + Json.decodeFromString(datePeriodSerializer, "{\"hours\":0}") + } + + private fun dateTimePeriodIso8601Serialization(dateTimePeriodSerializer: KSerializer) { + for ((period, json) in listOf( + Pair(DateTimePeriod(), "\"P0D\""), + Pair(DateTimePeriod(hours = 1), "\"PT1H\""), + Pair(DateTimePeriod(days = 1, hours = -1), "\"P1DT-1H\""), + Pair(DateTimePeriod(days = -1, hours = -1), "\"-P1DT1H\""), + Pair(DateTimePeriod(months = -1), "\"-P1M\""), + Pair(DateTimePeriod(years = -1, months = -2, days = -3, hours = -4, minutes = -5, seconds = 0, nanoseconds = 500_000_000), + "\"-P1Y2M3DT4H4M59.500000000S\""), + )) { + assertEquals(json, Json.encodeToString(dateTimePeriodSerializer, period)) + assertEquals(period, Json.decodeFromString(dateTimePeriodSerializer, json)) + } + } + + private fun dateTimePeriodComponentSerialization(dateTimePeriodSerializer: KSerializer) { + for ((period, json) in listOf( + Pair(DateTimePeriod(), "{}"), + Pair(DateTimePeriod(hours = 1), "{\"hours\":1}"), + Pair(DateTimePeriod(days = 1, hours = -1), "{\"days\":1,\"hours\":-1}"), + Pair(DateTimePeriod(days = -1, hours = -1), "{\"days\":-1,\"hours\":-1}"), + Pair(DateTimePeriod(months = -1), "{\"months\":-1}"), + Pair(DateTimePeriod(years = -1, months = -2, days = -3, hours = -4, minutes = -5, seconds = 0, nanoseconds = 500_000_000), + "{\"years\":-1,\"months\":-2,\"days\":-3,\"hours\":-4,\"minutes\":-4,\"seconds\":-59,\"nanoseconds\":-500000000}"), + )) { + assertEquals(json, Json.encodeToString(dateTimePeriodSerializer, period)) + assertEquals(period, Json.decodeFromString(dateTimePeriodSerializer, json)) + } + } + + @Test + fun testDatePeriodIso8601Serialization() { + datePeriodIso8601Serialization(DatePeriodIso8601Serializer, DateTimePeriodIso8601Serializer) + } + + @Test + fun testDatePeriodComponentSerialization() { + datePeriodComponentSerialization(DatePeriodComponentSerializer, DateTimePeriodComponentSerializer) + } + + @Test + fun testDateTimePeriodIso8601Serialization() { + dateTimePeriodIso8601Serialization(DateTimePeriodIso8601Serializer) + } + + @Test + fun testDateTimePeriodComponentSerialization() { + dateTimePeriodComponentSerialization(DateTimePeriodComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // Check that they behave the same as the ISO-8601 serializers + dateTimePeriodIso8601Serialization(Json.serializersModule.serializer()) + datePeriodIso8601Serialization(Json.serializersModule.serializer(), Json.serializersModule.serializer()) + } + +} diff --git a/serialization/common/test/DateTimeUnitSerializationTest.kt b/serialization/common/test/DateTimeUnitSerializationTest.kt new file mode 100644 index 000000000..f63fcab87 --- /dev/null +++ b/serialization/common/test/DateTimeUnitSerializationTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.* +import kotlinx.serialization.serializer +import kotlin.random.* +import kotlin.test.* + +class DateTimeUnitSerializationTest { + private fun timeBasedSerialization(serializer: KSerializer) { + repeat(100) { + val nanoseconds = Random.nextLong(1, Long.MAX_VALUE) + val unit = DateTimeUnit.TimeBased(nanoseconds) + val json = "{\"nanoseconds\":${nanoseconds.toString()}}" // https://youtrack.jetbrains.com/issue/KT-39891 + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + } + + private fun dayBasedSerialization(serializer: KSerializer) { + repeat(100) { + val days = Random.nextInt(1, Int.MAX_VALUE) + val unit = DateTimeUnit.DateBased.DayBased(days) + val json = "{\"days\":$days}" + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + } + + private fun monthBasedSerialization(serializer: KSerializer) { + repeat(100) { + val months = Random.nextInt(1, Int.MAX_VALUE) + val unit = DateTimeUnit.DateBased.MonthBased(months) + val json = "{\"months\":$months}" + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + } + + private fun dateBasedSerialization(serializer: KSerializer) { + repeat(100) { + val days = Random.nextInt(1, Int.MAX_VALUE) + val unit = DateTimeUnit.DateBased.DayBased(days) + val json = "{\"type\":\"DayBased\",\"days\":$days}" + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + repeat(100) { + val months = Random.nextInt(1, Int.MAX_VALUE) + val unit = DateTimeUnit.DateBased.MonthBased(months) + val json = "{\"type\":\"MonthBased\",\"months\":$months}" + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + } + + private fun serialization(serializer: KSerializer) { + repeat(100) { + val nanoseconds = Random.nextLong(1, Long.MAX_VALUE) + val unit = DateTimeUnit.TimeBased(nanoseconds) + val json = "{\"type\":\"TimeBased\",\"nanoseconds\":${nanoseconds.toString()}}" // https://youtrack.jetbrains.com/issue/KT-39891 + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + repeat(100) { + val days = Random.nextInt(1, Int.MAX_VALUE) + val unit = DateTimeUnit.DateBased.DayBased(days) + val json = "{\"type\":\"DayBased\",\"days\":$days}" + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + repeat(100) { + val months = Random.nextInt(1, Int.MAX_VALUE) + val unit = DateTimeUnit.DateBased.MonthBased(months) + val json = "{\"type\":\"MonthBased\",\"months\":$months}" + assertEquals(json, Json.encodeToString(serializer, unit)) + assertEquals(unit, Json.decodeFromString(serializer, json)) + } + } + + @Test + fun testTimeBasedUnitSerialization() { + timeBasedSerialization(TimeBasedDateTimeUnitSerializer) + } + + @Test + fun testDayBasedSerialization() { + dayBasedSerialization(DayBasedDateTimeUnitSerializer) + } + + @Test + fun testMonthBasedSerialization() { + monthBasedSerialization(MonthBasedDateTimeUnitSerializer) + } + + @Test + fun testDateBasedSerialization() { + dateBasedSerialization(DateBasedDateTimeUnitSerializer) + } + + @Test + fun testSerialization() { + serialization(DateTimeUnitSerializer) + } + + @Test + fun testDefaultSerializers() { + monthBasedSerialization(Json.serializersModule.serializer()) + timeBasedSerialization(Json.serializersModule.serializer()) + dayBasedSerialization(Json.serializersModule.serializer()) + dateBasedSerialization(Json.serializersModule.serializer()) + serialization(Json.serializersModule.serializer()) + } + +} \ No newline at end of file diff --git a/serialization/common/test/DayOfWeekSerializationTest.kt b/serialization/common/test/DayOfWeekSerializationTest.kt new file mode 100644 index 000000000..18470976b --- /dev/null +++ b/serialization/common/test/DayOfWeekSerializationTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.json.* +import kotlin.test.* + +class DayOfWeekSerializationTest { + @Test + fun testSerialization() { + for (dayOfWeek in DayOfWeek.values()) { + val json = "\"${dayOfWeek.name}\"" + assertEquals(json, Json.encodeToString(DayOfWeekSerializer, dayOfWeek)) + assertEquals(dayOfWeek, Json.decodeFromString(DayOfWeekSerializer, json)) + } + } +} \ No newline at end of file diff --git a/serialization/common/test/InstantSerializationTest.kt b/serialization/common/test/InstantSerializationTest.kt new file mode 100644 index 000000000..ffb92cd8b --- /dev/null +++ b/serialization/common/test/InstantSerializationTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlin.test.* + +class InstantSerializationTest { + + private fun iso8601Serialization(serializer: KSerializer) { + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416, 124000), + "\"2020-12-09T09:16:56.000124Z\""), + Pair(Instant.fromEpochSeconds(-1607505416, -124000), + "\"1919-01-23T14:43:03.999876Z\""), + Pair(Instant.fromEpochSeconds(987654321, 123456789), + "\"2001-04-19T04:25:21.123456789Z\""), + Pair(Instant.fromEpochSeconds(987654321, 0), + "\"2001-04-19T04:25:21Z\""), + )) { + assertEquals(json, Json.encodeToString(serializer, instant)) + assertEquals(instant, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416, 124000), + "{\"epochSeconds\":1607505416,\"nanosecondsOfSecond\":124000}"), + Pair(Instant.fromEpochSeconds(-1607505416, -124000), + "{\"epochSeconds\":-1607505417,\"nanosecondsOfSecond\":999876000}"), + Pair(Instant.fromEpochSeconds(987654321, 123456789), + "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":123456789}"), + Pair(Instant.fromEpochSeconds(987654321, 0), + "{\"epochSeconds\":987654321}"), + )) { + assertEquals(json, Json.encodeToString(serializer, instant)) + assertEquals(instant, Json.decodeFromString(serializer, json)) + } + // check that having a `"nanosecondsOfSecond": 0` field doesn't break deserialization + assertEquals(Instant.fromEpochSeconds(987654321, 0), + Json.decodeFromString(serializer, + "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":0}")) + // "epochSeconds" should always be present + assertFailsWith { Json.decodeFromString(serializer, "{}") } + assertFailsWith { Json.decodeFromString(serializer, "{\"nanosecondsOfSecond\":3}") } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(InstantIso8601Serializer) + } + + @Test + fun testComponentSerialization() { + componentSerialization(InstantComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO-8601 + iso8601Serialization(Json.serializersModule.serializer()) + } +} \ No newline at end of file diff --git a/serialization/common/test/IntegrationTest.kt b/serialization/common/test/IntegrationTest.kt new file mode 100644 index 000000000..0452a10fd --- /dev/null +++ b/serialization/common/test/IntegrationTest.kt @@ -0,0 +1,130 @@ +/* + * 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import kotlin.test.Test +import kotlin.test.assertEquals + +class IntegrationTest { + + @Serializable + data class Dummy( + @Contextual val instant: Instant, + @Contextual val date: LocalDate, + @Contextual val dateTime: LocalDateTime, + @Contextual val datePeriod: DatePeriod, + @Contextual val dateTimePeriod: DateTimePeriod, + // @Contextual val dayOfWeek: DayOfWeek, // doesn't compile on Native + // @Contextual val month: Month, + ) + + private val module = SerializersModule { + contextual(InstantComponentSerializer) + contextual(LocalDateComponentSerializer) + contextual(LocalDateTimeComponentSerializer) + contextual(DatePeriodComponentSerializer) + contextual(DateTimePeriodComponentSerializer) + contextual(DayOfWeekSerializer) + contextual(MonthSerializer) + } + + private val format = Json { serializersModule = module } + + @Test + fun testContextualSerialization() { + val dummyValue = Dummy( + Instant.parse("2021-03-24T01:29:30.123456789Z"), + LocalDate.parse("2020-01-02"), + LocalDateTime.parse("2020-01-03T12:59:58.010203045"), + DatePeriod.parse("P20Y-2M-3D"), + DateTimePeriod.parse("-P50Y-1M-2DT3H4M5.0123S"), + // DayOfWeek.MONDAY, // doesn't compile on Native + // Month.DECEMBER, + ) + val json = """{"instant":{"epochSeconds":1616549370,"nanosecondsOfSecond":123456789},""" + + """"date":{"year":2020,"month":1,"day":2},""" + + """"dateTime":{"year":2020,"month":1,"day":3,"hour":12,"minute":59,"second":58,"nanosecond":10203045},""" + + """"datePeriod":{"years":19,"months":10,"days":-3},""" + + """"dateTimePeriod":{"years":-49,"months":-11,"days":2,"hours":-3,"minutes":-4,"seconds":-5,"nanoseconds":-12300000}}""" + assertEquals(dummyValue, format.decodeFromString(json)) + assertEquals(json, format.encodeToString(dummyValue)) + } + + @Serializable + data class Dummy2( + val instant: Instant, + val date: LocalDate, + val dateTime: LocalDateTime, + val datePeriod: DatePeriod, + val dateTimePeriod: DateTimePeriod, + // val dayOfWeek: DayOfWeek, + // val month: Month, + ) + + @Test + fun testDefaultSerialization() { + val dummyValue = Dummy2( + Instant.parse("2021-03-24T01:29:30.123456789Z"), + LocalDate.parse("2020-01-02"), + LocalDateTime.parse("2020-01-03T12:59:58.010203045"), + DatePeriod.parse("P20Y-2M-3D"), + DateTimePeriod.parse("-P50Y-1M-2DT3H4M5.0123S"), + // DayOfWeek.MONDAY, + // Month.DECEMBER, + ) + val json = "{\"instant\":\"2021-03-24T01:29:30.123456789Z\"," + + "\"date\":\"2020-01-02\"," + + "\"dateTime\":\"2020-01-03T12:59:58.010203045\"," + + "\"datePeriod\":\"P19Y10M-3D\"," + + "\"dateTimePeriod\":\"P-49Y-11M2DT-3H-4M-5.012300000S\"" + + "}" + assertEquals(dummyValue, Json.decodeFromString(json)) + assertEquals(json, Json.encodeToString(dummyValue)) + } + + @Serializable + data class Dummy3( + @Serializable(with = InstantComponentSerializer::class) val instant: Instant, + @Serializable(with = LocalDateComponentSerializer::class) val date: LocalDate, + @Serializable(with = LocalDateTimeComponentSerializer::class) val dateTime: LocalDateTime, + @Serializable(with = DatePeriodComponentSerializer::class) val datePeriod: DatePeriod, + @Serializable(with = DateTimePeriodComponentSerializer::class) val dateTimePeriod: DateTimePeriod, + @Serializable(with = DayOfWeekSerializer::class) val dayOfWeek: DayOfWeek, + @Serializable(with = MonthSerializer::class) val month: Month, + ) + + @Test + fun testExplicitSerializerSpecification() { + val dummyValue = Dummy3( + Instant.parse("2021-03-24T01:29:30.123456789Z"), + LocalDate.parse("2020-01-02"), + LocalDateTime.parse("2020-01-03T12:59:58.010203045"), + DatePeriod.parse("P20Y-2M-3D"), + DateTimePeriod.parse("-P50Y-1M-2DT3H4M5.0123S"), + DayOfWeek.MONDAY, + Month.DECEMBER, + ) + val json = """{"instant":{"epochSeconds":1616549370,"nanosecondsOfSecond":123456789},""" + + """"date":{"year":2020,"month":1,"day":2},""" + + """"dateTime":{"year":2020,"month":1,"day":3,"hour":12,"minute":59,"second":58,"nanosecond":10203045},""" + + """"datePeriod":{"years":19,"months":10,"days":-3},""" + + """"dateTimePeriod":{"years":-49,"months":-11,"days":2,"hours":-3,"minutes":-4,"seconds":-5,"nanoseconds":-12300000},""" + + """"dayOfWeek":"MONDAY",""" + + """"month":"DECEMBER"""" + + """}""" + assertEquals(dummyValue, format.decodeFromString(json)) + assertEquals(json, format.encodeToString(dummyValue)) + } +} \ No newline at end of file diff --git a/serialization/common/test/LocalDateSerializationTest.kt b/serialization/common/test/LocalDateSerializationTest.kt new file mode 100644 index 000000000..1dd953352 --- /dev/null +++ b/serialization/common/test/LocalDateSerializationTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlin.test.* + +class LocalDateSerializationTest { + private fun iso8601Serialization(serializer: KSerializer) { + for ((localDate, json) in listOf( + Pair(LocalDate(2020, 12, 9), "\"2020-12-09\""), + Pair(LocalDate(-2020, 1, 1), "\"-2020-01-01\""), + Pair(LocalDate(2019, 10, 1), "\"2019-10-01\""), + )) { + assertEquals(json, Json.encodeToString(serializer, localDate)) + assertEquals(localDate, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((localDate, json) in listOf( + Pair(LocalDate(2020, 12, 9), "{\"year\":2020,\"month\":12,\"day\":9}"), + Pair(LocalDate(-2020, 1, 1), "{\"year\":-2020,\"month\":1,\"day\":1}"), + Pair(LocalDate(2019, 10, 1), "{\"year\":2019,\"month\":10,\"day\":1}"), + )) { + assertEquals(json, Json.encodeToString(serializer, localDate)) + assertEquals(localDate, Json.decodeFromString(serializer, json)) + } + // all components must be present + assertFailsWith { + Json.decodeFromString(serializer, "{}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":3,\"month\":12}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":3,\"day\":12}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"month\":3,\"day\":12}") + } + // invalid values must fail to construct + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":1000000000000,\"month\":3,\"day\":12}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":2020,\"month\":30,\"day\":12}") + } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(LocalDateIso8601Serializer) + } + + @Test + fun testComponentSerialization() { + componentSerialization(LocalDateComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO-8601 + iso8601Serialization(Json.serializersModule.serializer()) + } + +} diff --git a/serialization/common/test/LocalDateTimeSerializationTest.kt b/serialization/common/test/LocalDateTimeSerializationTest.kt new file mode 100644 index 000000000..f01254d8a --- /dev/null +++ b/serialization/common/test/LocalDateTimeSerializationTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.* +import kotlinx.serialization.serializer +import kotlin.test.* + +class LocalDateTimeSerializationTest { + private fun iso8601Serialization(serializer: KSerializer) { + for ((localDateTime, json) in listOf( + Pair(LocalDateTime(2008, 7, 5, 2, 1), "\"2008-07-05T02:01\""), + Pair(LocalDateTime(2007, 12, 31, 23, 59, 1), "\"2007-12-31T23:59:01\""), + Pair(LocalDateTime(999, 12, 31, 23, 59, 59, 990000000), "\"0999-12-31T23:59:59.990\""), + Pair(LocalDateTime(-1, 1, 2, 23, 59, 59, 999990000), "\"-0001-01-02T23:59:59.999990\""), + Pair(LocalDateTime(-2008, 1, 2, 23, 59, 59, 999999990), "\"-2008-01-02T23:59:59.999999990\""), + )) { + assertEquals(json, Json.encodeToString(serializer, localDateTime)) + assertEquals(localDateTime, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((localDateTime, json) in listOf( + Pair(LocalDateTime(2008, 7, 5, 2, 1), "{\"year\":2008,\"month\":7,\"day\":5,\"hour\":2,\"minute\":1}"), + Pair(LocalDateTime(2007, 12, 31, 23, 59, 1), + "{\"year\":2007,\"month\":12,\"day\":31,\"hour\":23,\"minute\":59,\"second\":1}"), + Pair(LocalDateTime(999, 12, 31, 23, 59, 59, 990000000), + "{\"year\":999,\"month\":12,\"day\":31,\"hour\":23,\"minute\":59,\"second\":59,\"nanosecond\":990000000}"), + Pair(LocalDateTime(-1, 1, 2, 23, 59, 59, 999990000), + "{\"year\":-1,\"month\":1,\"day\":2,\"hour\":23,\"minute\":59,\"second\":59,\"nanosecond\":999990000}"), + Pair(LocalDateTime(-2008, 1, 2, 23, 59, 59, 999999990), + "{\"year\":-2008,\"month\":1,\"day\":2,\"hour\":23,\"minute\":59,\"second\":59,\"nanosecond\":999999990}"), + Pair(LocalDateTime(-2008, 1, 2, 23, 59, 0, 1), + "{\"year\":-2008,\"month\":1,\"day\":2,\"hour\":23,\"minute\":59,\"second\":0,\"nanosecond\":1}"), + )) { + assertEquals(json, Json.encodeToString(serializer, localDateTime)) + assertEquals(localDateTime, Json.decodeFromString(serializer, json)) + } + // adding omitted values shouldn't break deserialization + assertEquals(LocalDateTime(2008, 7, 5, 2, 1), + Json.decodeFromString(serializer, + "{\"year\":2008,\"month\":7,\"day\":5,\"hour\":2,\"minute\":1,\"second\":0}" + )) + assertEquals(LocalDateTime(2008, 7, 5, 2, 1), + Json.decodeFromString(serializer, + "{\"year\":2008,\"month\":7,\"day\":5,\"hour\":2,\"minute\":1,\"nanosecond\":0}" + )) + assertEquals(LocalDateTime(2008, 7, 5, 2, 1), + Json.decodeFromString(serializer, + "{\"year\":2008,\"month\":7,\"day\":5,\"hour\":2,\"minute\":1,\"second\":0,\"nanosecond\":0}" + )) + // invalid values must fail to construct + assertFailsWith { + Json.decodeFromString(serializer, + "{\"year\":1000000000000,\"month\":3,\"day\":12,\"hour\":10,\"minute\":2}") + } + assertFailsWith { + Json.decodeFromString(serializer, + "{\"year\":2020,\"month\":30,\"day\":12,\"hour\":10,\"minute\":2}") + } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(LocalDateTimeIso8601Serializer) + } + + @Test + fun testComponentSerialization() { + componentSerialization(LocalDateTimeComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO-8601 + iso8601Serialization(Json.serializersModule.serializer()) + } +} diff --git a/serialization/common/test/MonthSerializationTest.kt b/serialization/common/test/MonthSerializationTest.kt new file mode 100644 index 000000000..8d8879b72 --- /dev/null +++ b/serialization/common/test/MonthSerializationTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.json.* +import kotlin.test.* + +class MonthSerializationTest { + @Test + fun testSerialization() { + for (month in Month.values()) { + val json = "\"${month.name}\"" + assertEquals(json, Json.encodeToString(MonthSerializer, month)) + assertEquals(month, Json.decodeFromString(MonthSerializer, json)) + } + } +} \ No newline at end of file diff --git a/serialization/common/test/TimeZoneSerializationTest.kt b/serialization/common/test/TimeZoneSerializationTest.kt new file mode 100644 index 000000000..9cb0f5058 --- /dev/null +++ b/serialization/common/test/TimeZoneSerializationTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2020 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.serialization.test + +import kotlinx.datetime.* +import kotlinx.datetime.serializers.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.json.* +import kotlinx.serialization.serializer +import kotlin.test.* + +class TimeZoneSerializationTest { + + private fun zoneOffsetSerialization(serializer: KSerializer) { + val offset2h = TimeZone.of("+02:00") as ZoneOffset + assertEquals("\"+02:00\"", Json.encodeToString(serializer, offset2h)) + assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02:00\"")) + assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02\"")) + assertEquals(offset2h, Json.decodeFromString(serializer, "\"+2\"")) + assertFailsWith { + Json.decodeFromString(serializer, "\"Europe/Berlin\"") + } + } + + private fun serialization(serializer: KSerializer) { + for (zoneId in listOf("Europe/Berlin", "+02:00")) { + val zone = TimeZone.of(zoneId) + val json = "\"$zoneId\"" + assertEquals(json, Json.encodeToString(serializer, zone)) + assertEquals(zone, Json.decodeFromString(serializer, json)) + } + } + + @Test + fun testZoneOffsetSerialization() { + zoneOffsetSerialization(ZoneOffsetSerializer) + } + + @Test + fun testSerialization() { + serialization(TimeZoneSerializer) + } + + @Test + fun testDefaultSerializers() { + zoneOffsetSerialization(Json.serializersModule.serializer()) + serialization(Json.serializersModule.serializer()) + } +} \ No newline at end of file diff --git a/serialization/js/test/JsJodaTimeZoneModule.kt b/serialization/js/test/JsJodaTimeZoneModule.kt new file mode 100644 index 000000000..f4805e10d --- /dev/null +++ b/serialization/js/test/JsJodaTimeZoneModule.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2019-2020 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.serialization.test + +@JsModule("@js-joda/timezone") +@JsNonModule +external object JsJodaTimeZoneModule + +private val jsJodaTz = JsJodaTimeZoneModule diff --git a/settings.gradle b/settings.gradle index 891045b26..28218e565 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,3 +13,5 @@ rootProject.name = 'Kotlin-DateTime-library' include ':core' project(":core").name='kotlinx-datetime' +include ':serialization' +project(":serialization").name='kotlinx-datetime-serialization'