diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index f9b2d3f3..5b5141d5 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -9,7 +9,9 @@ import kotlinx.datetime.internal.* import kotlinx.datetime.serializers.DatePeriodIso8601Serializer import kotlinx.datetime.serializers.DateTimePeriodIso8601Serializer import kotlinx.datetime.serializers.DatePeriodComponentSerializer +import kotlinx.datetime.serializers.DatePeriodSerializer import kotlinx.datetime.serializers.DateTimePeriodComponentSerializer +import kotlinx.datetime.serializers.DateTimePeriodSerializer import kotlin.math.* import kotlin.time.Duration import kotlinx.serialization.Serializable @@ -63,13 +65,14 @@ import kotlinx.serialization.Serializable * `DateTimePeriod` can also be returned as the result of instant arithmetic operations (see [Instant.periodUntil]). * * Additionally, there are several `kotlinx-serialization` serializers for [DateTimePeriod]: - * - [DateTimePeriodIso8601Serializer] for the ISO 8601 format; + * - The default serializer, delegating to [toString] and [parse]. + * - [DateTimePeriodIso8601Serializer] for the ISO 8601 format. * - [DateTimePeriodComponentSerializer] for an object with components. * * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.construction * @sample kotlinx.datetime.test.samples.DateTimePeriodSamples.simpleParsingAndFormatting */ -@Serializable(with = DateTimePeriodIso8601Serializer::class) +@Serializable(with = DateTimePeriodSerializer::class) // TODO: could be error-prone without explicitly named params public sealed class DateTimePeriod { internal abstract val totalMonths: Int @@ -426,7 +429,7 @@ public fun String.toDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(this * * @sample kotlinx.datetime.test.samples.DatePeriodSamples.simpleParsingAndFormatting */ -@Serializable(with = DatePeriodIso8601Serializer::class) +@Serializable(with = DatePeriodSerializer::class) public class DatePeriod internal constructor( internal override val totalMonths: Int, override val days: Int, diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 36fd40d2..7437bd1c 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -7,8 +7,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.InstantIso8601Serializer -import kotlinx.datetime.serializers.InstantComponentSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlin.time.* @@ -189,12 +188,13 @@ import kotlin.time.* * ``` * * Additionally, there are several `kotlinx-serialization` serializers for [Instant]: + * - The default serializer, delegating to [toString] and [parse]. * - [InstantIso8601Serializer] for the ISO 8601 extended format. * - [InstantComponentSerializer] for an object with components. * * @see LocalDateTime for a user-visible representation of moments in time in an unspecified time zone. */ -@Serializable(with = InstantIso8601Serializer::class) +@Serializable(with = InstantSerializer::class) public expect class Instant : Comparable { /** diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 0146d6f9..eb80dc01 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -56,6 +56,7 @@ import kotlinx.serialization.Serializable * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDate]: + * - The default serializer, delegating to [toString] and [parse]. * - [LocalDateIso8601Serializer] for the ISO 8601 extended format. * - [LocalDateComponentSerializer] for an object with components. * @@ -64,7 +65,7 @@ import kotlinx.serialization.Serializable * @sample kotlinx.datetime.test.samples.LocalDateSamples.simpleParsingAndFormatting * @sample kotlinx.datetime.test.samples.LocalDateSamples.customFormat */ -@Serializable(with = LocalDateIso8601Serializer::class) +@Serializable(with = LocalDateSerializer::class) public expect class LocalDate : Comparable { public companion object { /** diff --git a/core/common/src/LocalDateTime.kt b/core/common/src/LocalDateTime.kt index db4533b8..09691d3b 100644 --- a/core/common/src/LocalDateTime.kt +++ b/core/common/src/LocalDateTime.kt @@ -6,8 +6,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer -import kotlinx.datetime.serializers.LocalDateTimeComponentSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable /** @@ -94,6 +93,7 @@ import kotlinx.serialization.Serializable * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalDateTime]: + * - The default serializer, delegating to [toString] and [parse]. * - [LocalDateTimeIso8601Serializer] for the ISO 8601 extended format. * - [LocalDateTimeComponentSerializer] for an object with components. * @@ -105,7 +105,7 @@ import kotlinx.serialization.Serializable * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.simpleParsingAndFormatting * @sample kotlinx.datetime.test.samples.LocalDateTimeSamples.customFormat */ -@Serializable(with = LocalDateTimeIso8601Serializer::class) +@Serializable(with = LocalDateTimeSerializer::class) public expect class LocalDateTime : Comparable { public companion object { diff --git a/core/common/src/LocalTime.kt b/core/common/src/LocalTime.kt index 695a1090..f27d6401 100644 --- a/core/common/src/LocalTime.kt +++ b/core/common/src/LocalTime.kt @@ -7,8 +7,7 @@ package kotlinx.datetime import kotlinx.datetime.LocalDate.Companion.parse import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalTimeIso8601Serializer -import kotlinx.datetime.serializers.LocalTimeComponentSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable @@ -68,6 +67,7 @@ import kotlinx.serialization.Serializable * See sample 4. * * Additionally, there are several `kotlinx-serialization` serializers for [LocalTime]: + * - The default serializer, delegating to [toString] and [parse]. * - [LocalTimeIso8601Serializer] for the ISO 8601 extended format, * - [LocalTimeComponentSerializer] for an object with components. * @@ -76,7 +76,7 @@ import kotlinx.serialization.Serializable * @sample kotlinx.datetime.test.samples.LocalTimeSamples.simpleParsingAndFormatting * @sample kotlinx.datetime.test.samples.LocalTimeSamples.customFormat */ -@Serializable(LocalTimeIso8601Serializer::class) +@Serializable(LocalTimeSerializer::class) public expect class LocalTime : Comparable { public companion object { diff --git a/core/common/src/UtcOffset.kt b/core/common/src/UtcOffset.kt index 97c5e410..8a5cc769 100644 --- a/core/common/src/UtcOffset.kt +++ b/core/common/src/UtcOffset.kt @@ -6,7 +6,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.UtcOffsetSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable /** @@ -47,12 +47,14 @@ import kotlinx.serialization.Serializable * [parse] and [UtcOffset.format] both support custom formats created with [Format] or defined in [Formats]. * See sample 3. * - * To serialize and deserialize [UtcOffset] values with `kotlinx-serialization`, use the [UtcOffsetSerializer]. + * To serialize and deserialize [UtcOffset] values with `kotlinx-serialization`, use the default serializer, + * or [UtcOffsetIso8601Serializer] for the ISO 8601 format explicitly. * * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.construction * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.simpleParsingAndFormatting * @sample kotlinx.datetime.test.samples.UtcOffsetSamples.customFormat */ +@Suppress("DEPRECATION") @Serializable(with = UtcOffsetSerializer::class) public expect class UtcOffset { /** diff --git a/core/common/src/serializers/DateTimePeriodSerializers.kt b/core/common/src/serializers/DateTimePeriodSerializers.kt index e5d904fc..ac76db34 100644 --- a/core/common/src/serializers/DateTimePeriodSerializers.kt +++ b/core/common/src/serializers/DateTimePeriodSerializers.kt @@ -19,7 +19,7 @@ import kotlinx.serialization.encoding.* public object DateTimePeriodComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("kotlinx.datetime.DateTimePeriod") { + buildClassSerialDescriptor("kotlinx.datetime.DateTimePeriod components") { element("years", isOptional = true) element("months", isOptional = true) element("days", isOptional = true) @@ -81,7 +81,7 @@ public object DateTimePeriodComponentSerializer: KSerializer { public object DateTimePeriodIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.datetime.DateTimePeriod", PrimitiveKind.STRING) + PrimitiveSerialDescriptor("kotlinx.datetime.DateTimePeriod ISO", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): DateTimePeriod = DateTimePeriod.parse(decoder.decodeString()) @@ -110,7 +110,7 @@ public object DatePeriodComponentSerializer: KSerializer { private fun unexpectedNonzero(fieldName: String, value: Int) = unexpectedNonzero(fieldName, value.toLong()) override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("kotlinx.datetime.DatePeriod") { + buildClassSerialDescriptor("kotlinx.datetime.DatePeriod components") { element("years", isOptional = true) element("months", isOptional = true) element("days", isOptional = true) @@ -166,7 +166,7 @@ public object DatePeriodComponentSerializer: KSerializer { public object DatePeriodIso8601Serializer: KSerializer { override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.datetime.DatePeriod", PrimitiveKind.STRING) + PrimitiveSerialDescriptor("kotlinx.datetime.DatePeriod ISO", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): DatePeriod = when (val period = DateTimePeriod.parse(decoder.decodeString())) { @@ -179,3 +179,15 @@ public object DatePeriodIso8601Serializer: KSerializer { } } + +@PublishedApi +internal object DateTimePeriodSerializer: KSerializer by DateTimePeriodIso8601Serializer { + override val descriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.DateTimePeriod", PrimitiveKind.STRING) +} + +@PublishedApi +internal object DatePeriodSerializer: KSerializer by DatePeriodIso8601Serializer { + override val descriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.DatePeriod", PrimitiveKind.STRING) +} diff --git a/core/common/src/serializers/InstantSerializers.kt b/core/common/src/serializers/InstantSerializers.kt index c64bdf47..1b3ccdd9 100644 --- a/core/common/src/serializers/InstantSerializers.kt +++ b/core/common/src/serializers/InstantSerializers.kt @@ -5,7 +5,9 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.Instant +import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -15,19 +17,18 @@ import kotlinx.serialization.encoding.* * * JSON example: `"2020-12-09T09:16:56.000124Z"` * - * @see Instant.toString - * @see Instant.parse + * @see DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET */ public object InstantIso8601Serializer : KSerializer { override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.datetime.Instant", PrimitiveKind.STRING) + PrimitiveSerialDescriptor("kotlinx.datetime.Instant ISO", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): Instant = - Instant.parse(decoder.decodeString()) + Instant.parse(decoder.decodeString(), DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET) override fun serialize(encoder: Encoder, value: Instant) { - encoder.encodeString(value.toString()) + encoder.encodeString(value.format(DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET)) } } @@ -40,7 +41,7 @@ public object InstantIso8601Serializer : KSerializer { public object InstantComponentSerializer : KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("kotlinx.datetime.Instant") { + buildClassSerialDescriptor("kotlinx.datetime.Instant components") { element("epochSeconds") element("nanosecondsOfSecond", isOptional = true) } @@ -75,3 +76,70 @@ public object InstantComponentSerializer : KSerializer { } } + +/** + * An abstract serializer for [Instant] values that uses + * a custom [DateTimeFormat] for serializing to and deserializing. + * + * [format] should be a format that includes enough components to unambiguously define a date, a time, and a UTC offset. + * See [Instant.parse] for details of how deserialization is performed. + * + * [name] is the name of the serializer. + * The [SerialDescriptor.serialName] of the resulting serializer is `kotlinx.datetime.Instant serializer `[name]. + * [SerialDescriptor.serialName] must be unique across all serializers in the same serialization context. + * When defining a serializer in a library, it is recommended to use the fully qualified class name in [name] + * to avoid conflicts with serializers defined by other libraries and client code. + * + * When serializing, the [Instant] value is formatted as a string using the specified [format] + * in the [ZERO][UtcOffset.ZERO] UTC offset. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * // serializes LocalDateTime(2008, 6, 30, 11, 5, 30).toInstant(TimeZone.UTC) + * // as the string "Mon, 30 Jun 2008 11:05:30 GMT" + * object Rfc1123InstantSerializer : FormattedInstantSerializer( + * "my.package.RFC1123", DateTimeComponents.Formats.RFC_1123 + * ) + * ``` + * + * Note that [Instant] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [InstantIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class FormattedInstantSerializer( + name: String, + private val format: DateTimeFormat, +) : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.Instant serializer $name", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Instant = + Instant.parse(decoder.decodeString(), format) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.format(format)) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun toString(): String = descriptor.serialName +} + +/** + * A serializer for [Instant] that uses the default [Instant.toString]/[Instant.parse]. + * + * JSON example: `"2020-12-09T09:16:56.000124Z"` + */ +@PublishedApi internal object InstantSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.Instant", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Instant = + Instant.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } + +} diff --git a/core/common/src/serializers/LocalDateSerializers.kt b/core/common/src/serializers/LocalDateSerializers.kt index e1c1c5e9..b684b44f 100644 --- a/core/common/src/serializers/LocalDateSerializers.kt +++ b/core/common/src/serializers/LocalDateSerializers.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serializers import kotlinx.datetime.LocalDate +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -15,22 +16,10 @@ import kotlinx.serialization.encoding.* * * JSON example: `"2020-01-01"` * - * @see LocalDate.parse - * @see LocalDate.toString + * @see LocalDate.Formats.ISO */ -public object LocalDateIso8601Serializer: KSerializer { - - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.datetime.LocalDate", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): LocalDate = - LocalDate.parse(decoder.decodeString()) - - override fun serialize(encoder: Encoder, value: LocalDate) { - encoder.encodeString(value.toString()) - } - -} +public object LocalDateIso8601Serializer : KSerializer +by LocalDate.Formats.ISO.asKSerializer("kotlinx.datetime.LocalDate ISO") /** * A serializer for [LocalDate] that represents a value as its components. @@ -40,7 +29,7 @@ public object LocalDateIso8601Serializer: KSerializer { public object LocalDateComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("kotlinx.datetime.LocalDate") { + buildClassSerialDescriptor("kotlinx.datetime.LocalDate components") { element("year") element("month") element("day") @@ -76,3 +65,63 @@ public object LocalDateComponentSerializer: KSerializer { } } + +/** + * An abstract serializer for [LocalDate] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * [name] is the name of the serializer. + * The [SerialDescriptor.serialName] of the resulting serializer is `kotlinx.datetime.LocalDate serializer `[name]. + * [SerialDescriptor.serialName] must be unique across all serializers in the same serialization context. + * When defining a serializer in a library, it is recommended to use the fully qualified class name in [name] + * to avoid conflicts with serializers defined by other libraries and client code. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * // serializes LocalDate(2020, 1, 4) as the string "20200104" + * object IsoBasicLocalDateSerializer : + * FormattedLocalDateSerializer("my.package.ISO_BASIC", LocalDate.Formats.ISO_BASIC) + * ``` + * + * Note that [LocalDate] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [LocalDateIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class FormattedLocalDateSerializer( + name: String, format: DateTimeFormat +) : KSerializer by format.asKSerializer("kotlinx.datetime.LocalDate serializer $name") + +internal fun DateTimeFormat.asKSerializer(serialName: String): KSerializer = + object : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): T = parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeString(format(value)) + } + + override fun toString(): String = serialName + } + +/** + * A serializer for [LocalDate] that uses the default [LocalDate.toString]/[LocalDate.parse]. + * + * JSON example: `"2020-01-01"` + */ +@PublishedApi +internal object LocalDateSerializer: KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.LocalDate", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): LocalDate = + LocalDate.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: LocalDate) { + encoder.encodeString(value.toString()) + } + +} diff --git a/core/common/src/serializers/LocalDateTimeSerializers.kt b/core/common/src/serializers/LocalDateTimeSerializers.kt index 73f29115..40f60742 100644 --- a/core/common/src/serializers/LocalDateTimeSerializers.kt +++ b/core/common/src/serializers/LocalDateTimeSerializers.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serializers import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -15,22 +16,10 @@ import kotlinx.serialization.encoding.* * * JSON example: `"2007-12-31T23:59:01"` * - * @see LocalDateTime.parse - * @see LocalDateTime.toString + * @see LocalDateTime.Formats.ISO */ -public object LocalDateTimeIso8601Serializer: KSerializer { - - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.datetime.LocalDateTime", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): LocalDateTime = - LocalDateTime.parse(decoder.decodeString()) - - override fun serialize(encoder: Encoder, value: LocalDateTime) { - encoder.encodeString(value.toString()) - } - -} +public object LocalDateTimeIso8601Serializer : KSerializer +by LocalDateTime.Formats.ISO.asKSerializer("kotlinx.datetime.LocalDateTime ISO") /** * A serializer for [LocalDateTime] that represents a value as its components. @@ -40,7 +29,7 @@ public object LocalDateTimeIso8601Serializer: KSerializer { public object LocalDateTimeComponentSerializer: KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("kotlinx.datetime.LocalDateTime") { + buildClassSerialDescriptor("kotlinx.datetime.LocalDateTime components") { element("year") element("month") element("day") @@ -98,3 +87,54 @@ public object LocalDateTimeComponentSerializer: KSerializer { } } + +/** + * An abstract serializer for [LocalDateTime] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * [name] is the name of the serializer. + * The [SerialDescriptor.serialName] of the resulting serializer is `kotlinx.datetime.LocalDateTime serializer `[name]. + * [SerialDescriptor.serialName] must be unique across all serializers in the same serialization context. + * When defining a serializer in a library, it is recommended to use the fully qualified class name in [name] + * to avoid conflicts with serializers defined by other libraries and client code. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * // serializes LocalDateTime(2020, 1, 4, 12, 30) as the string "2020-01-04 12:30" + * object PythonDateTimeSerializer : FormattedLocalDateTimeSerializer("my.package.PythonDateTime", + * LocalDateTime.Format { + * date(LocalDate.Formats.ISO) + * char(' ') + * time(LocalTime.Formats.ISO) + * } + * ) + * ``` + * + * Note that [LocalDateTime] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [LocalDateTimeIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class FormattedLocalDateTimeSerializer( + name: String, format: DateTimeFormat +) : KSerializer by format.asKSerializer("kotlinx.datetime.LocalDateTime serializer $name") + +/** + * A serializer for [LocalDateTime] that uses the default [LocalDateTime.toString]/[LocalDateTime.parse]. + * + * JSON example: `"2007-12-31T23:59:01"` + */ +@PublishedApi +internal object LocalDateTimeSerializer: KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.LocalDateTime", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): LocalDateTime = + LocalDateTime.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: LocalDateTime) { + encoder.encodeString(value.toString()) + } + +} diff --git a/core/common/src/serializers/LocalTimeSerializers.kt b/core/common/src/serializers/LocalTimeSerializers.kt index b8c1c0eb..d67a6350 100644 --- a/core/common/src/serializers/LocalTimeSerializers.kt +++ b/core/common/src/serializers/LocalTimeSerializers.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serializers import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -15,21 +16,10 @@ import kotlinx.serialization.encoding.* * * JSON example: `"12:01:03.999"` * - * @see LocalDate.parse - * @see LocalDate.toString + * @see LocalTime.Formats.ISO */ -public object LocalTimeIso8601Serializer : KSerializer { - - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("kotlinx.datetime.LocalTime", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): LocalTime = - LocalTime.parse(decoder.decodeString()) - - override fun serialize(encoder: Encoder, value: LocalTime) { - encoder.encodeString(value.toString()) - } -} +public object LocalTimeIso8601Serializer : KSerializer +by LocalTime.Formats.ISO.asKSerializer("kotlinx.datetime.LocalTime ISO") /** * A serializer for [LocalTime] that represents a value as its components. @@ -39,7 +29,7 @@ public object LocalTimeIso8601Serializer : KSerializer { public object LocalTimeComponentSerializer : KSerializer { override val descriptor: SerialDescriptor = - buildClassSerialDescriptor("kotlinx.datetime.LocalTime") { + buildClassSerialDescriptor("kotlinx.datetime.LocalTime components") { element("hour") element("minute") element("second", isOptional = true) @@ -81,3 +71,52 @@ public object LocalTimeComponentSerializer : KSerializer { } } } + +/** + * An abstract serializer for [LocalTime] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * [name] is the name of the serializer. + * The [SerialDescriptor.serialName] of the resulting serializer is `kotlinx.datetime.LocalTime serializer `[name]. + * [SerialDescriptor.serialName] must be unique across all serializers in the same serialization context. + * When defining a serializer in a library, it is recommended to use the fully qualified class name in [name] + * to avoid conflicts with serializers defined by other libraries and client code. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * // serializes LocalTime(12, 30) as "12:30:00.000" + * object FixedWidthTimeSerializer : FormattedLocalTimeSerializer("my.package.FixedWidthTime", LocalTime.Format { + * hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + * }) + * ``` + * + * Note that [LocalTime] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [LocalTimeIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class FormattedLocalTimeSerializer( + name: String, format: DateTimeFormat +) : KSerializer by format.asKSerializer("kotlinx.datetime.LocalTime serializer $name") + +/** + * A serializer for [LocalTime] that uses the ISO 8601 representation. + * + * JSON example: `"12:01:03.999"` + * + * @see LocalDate.parse + * @see LocalDate.toString + */ +@PublishedApi +internal object LocalTimeSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.LocalTime", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): LocalTime = + LocalTime.parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: LocalTime) { + encoder.encodeString(value.toString()) + } +} diff --git a/core/common/src/serializers/TimeZoneSerializers.kt b/core/common/src/serializers/TimeZoneSerializers.kt index 2d6d1c38..538aee5b 100644 --- a/core/common/src/serializers/TimeZoneSerializers.kt +++ b/core/common/src/serializers/TimeZoneSerializers.kt @@ -5,9 +5,8 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.FixedOffsetTimeZone -import kotlinx.datetime.TimeZone -import kotlinx.datetime.UtcOffset +import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -58,9 +57,17 @@ public object FixedOffsetTimeZoneSerializer: KSerializer { * * JSON example: `"+02:00"` * - * @see UtcOffset.parse - * @see UtcOffset.toString + * @see UtcOffset.Formats.ISO */ +public object UtcOffsetIso8601Serializer : KSerializer +by UtcOffset.Formats.ISO.asKSerializer("kotlinx.datetime.UtcOffset ISO") + +/** + * A serializer for [UtcOffset] that uses the default [UtcOffset.toString]/[UtcOffset.parse]. + * + * JSON example: `"+02:00"` + */ +@Deprecated("Use UtcOffset.serializer() instead", ReplaceWith("UtcOffset.serializer()")) public object UtcOffsetSerializer: KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlinx.datetime.UtcOffset", PrimitiveKind.STRING) @@ -74,3 +81,30 @@ public object UtcOffsetSerializer: KSerializer { } } + +/** + * An abstract serializer for [UtcOffset] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * [name] is the name of the serializer. + * The [SerialDescriptor.serialName] of the resulting serializer is `kotlinx.datetime.UtcOffset serializer `[name]. + * [SerialDescriptor.serialName] must be unique across all serializers in the same serialization context. + * When defining a serializer in a library, it is recommended to use the fully qualified class name in [name] + * to avoid conflicts with serializers defined by other libraries and client code. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * // serializes the UTC offset UtcOffset(hours = 2) as the string "+0200" + * object FourDigitOffsetSerializer : FormattedUtcOffsetSerializer( + * "my.package.FOUR_DIGITS", UtcOffset.Formats.FOUR_DIGITS + * ) + * ``` + * + * Note that [UtcOffset] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [UtcOffsetSerializer] is provided for the ISO 8601 format. + */ +public abstract class FormattedUtcOffsetSerializer( + name: String, format: DateTimeFormat +) : KSerializer by format.asKSerializer("kotlinx.datetime.UtcOffset serializer $name") diff --git a/core/commonJs/src/Instant.kt b/core/commonJs/src/Instant.kt index af7cc868..45c6c2f8 100644 --- a/core/commonJs/src/Instant.kt +++ b/core/commonJs/src/Instant.kt @@ -13,13 +13,13 @@ import kotlinx.datetime.internal.JSJoda.ChronoUnit as jtChronoUnit import kotlinx.datetime.internal.JSJoda.ZonedDateTime as jtZonedDateTime import kotlinx.datetime.internal.safeAdd import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlin.time.* import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds -@Serializable(with = InstantIso8601Serializer::class) +@Serializable(with = InstantSerializer::class) public actual class Instant internal constructor(internal val value: jtInstant) : Comparable { public actual val epochSeconds: Long diff --git a/core/commonJs/src/LocalDate.kt b/core/commonJs/src/LocalDate.kt index a650665d..2f6285cb 100644 --- a/core/commonJs/src/LocalDate.kt +++ b/core/commonJs/src/LocalDate.kt @@ -6,12 +6,12 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.LocalDate as jtLocalDate import kotlinx.datetime.internal.JSJoda.ChronoUnit as jtChronoUnit -@Serializable(with = LocalDateIso8601Serializer::class) +@Serializable(with = LocalDateSerializer::class) public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { public actual companion object { diff --git a/core/commonJs/src/LocalDateTime.kt b/core/commonJs/src/LocalDateTime.kt index c85e772a..59dfaaab 100644 --- a/core/commonJs/src/LocalDateTime.kt +++ b/core/commonJs/src/LocalDateTime.kt @@ -7,11 +7,11 @@ package kotlinx.datetime import kotlinx.datetime.format.* import kotlinx.datetime.format.ISO_DATETIME import kotlinx.datetime.format.LocalDateTimeFormat -import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.LocalDateTime as jtLocalDateTime -@Serializable(with = LocalDateTimeIso8601Serializer::class) +@Serializable(with = LocalDateTimeSerializer::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/commonJs/src/LocalTime.kt b/core/commonJs/src/LocalTime.kt index 674e5cb6..72aab223 100644 --- a/core/commonJs/src/LocalTime.kt +++ b/core/commonJs/src/LocalTime.kt @@ -8,11 +8,11 @@ import kotlinx.datetime.format.* import kotlinx.datetime.format.ISO_TIME import kotlinx.datetime.format.LocalTimeFormat import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.LocalTimeIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.LocalTime as jtLocalTime -@Serializable(LocalTimeIso8601Serializer::class) +@Serializable(LocalTimeSerializer::class) public actual class LocalTime internal constructor(internal val value: jtLocalTime) : Comparable { diff --git a/core/commonJs/src/UtcOffset.kt b/core/commonJs/src/UtcOffset.kt index 65dcf865..8f0ed74e 100644 --- a/core/commonJs/src/UtcOffset.kt +++ b/core/commonJs/src/UtcOffset.kt @@ -11,9 +11,10 @@ import kotlinx.datetime.internal.JSJoda.DateTimeFormatterBuilder as jtDateTimeFo import kotlinx.datetime.internal.JSJoda.DateTimeFormatter as jtDateTimeFormatter import kotlinx.datetime.internal.JSJoda.ResolverStyle as jtResolverStyle import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.UtcOffsetSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable +@Suppress("DEPRECATION") @Serializable(with = UtcOffsetSerializer::class) public actual class UtcOffset internal constructor(internal val zoneOffset: jtZoneOffset) { public actual val totalSeconds: Int get() = zoneOffset.totalSeconds() diff --git a/core/jvm/src/Instant.kt b/core/jvm/src/Instant.kt index ab347464..b7783206 100644 --- a/core/jvm/src/Instant.kt +++ b/core/jvm/src/Instant.kt @@ -9,7 +9,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* import kotlinx.datetime.internal.safeMultiply import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.temporal.* @@ -19,7 +19,7 @@ import kotlin.time.Duration.Companion.seconds import java.time.Instant as jtInstant import java.time.Clock as jtClock -@Serializable(with = InstantIso8601Serializer::class) +@Serializable(with = InstantSerializer::class) public actual class Instant internal constructor(internal val value: jtInstant) : Comparable { public actual val epochSeconds: Long diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index fe3b9ae1..c0d2be84 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -9,14 +9,14 @@ import kotlinx.datetime.format.* import kotlinx.datetime.internal.safeAdd import kotlinx.datetime.internal.safeMultiply import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.datetime.serializers.* 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) +@Serializable(with = LocalDateSerializer::class) public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable { public actual companion object { public actual fun parse(input: CharSequence, format: DateTimeFormat): LocalDate = diff --git a/core/jvm/src/LocalDateTime.kt b/core/jvm/src/LocalDateTime.kt index 7dc28cdb..54f2b3de 100644 --- a/core/jvm/src/LocalDateTime.kt +++ b/core/jvm/src/LocalDateTime.kt @@ -6,7 +6,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalDateTimeIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.format.DateTimeParseException @@ -15,7 +15,7 @@ import java.time.LocalDateTime as jtLocalDateTime public actual typealias Month = java.time.Month public actual typealias DayOfWeek = java.time.DayOfWeek -@Serializable(with = LocalDateTimeIso8601Serializer::class) +@Serializable(with = LocalDateTimeSerializer::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/LocalTime.kt b/core/jvm/src/LocalTime.kt index 71052570..fe67736d 100644 --- a/core/jvm/src/LocalTime.kt +++ b/core/jvm/src/LocalTime.kt @@ -8,13 +8,13 @@ package kotlinx.datetime import kotlinx.datetime.format.* import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.LocalTimeIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.format.DateTimeParseException import java.time.LocalTime as jtLocalTime -@Serializable(with = LocalTimeIso8601Serializer::class) +@Serializable(with = LocalTimeSerializer::class) public actual class LocalTime internal constructor(internal val value: jtLocalTime) : Comparable { diff --git a/core/jvm/src/UtcOffsetJvm.kt b/core/jvm/src/UtcOffsetJvm.kt index 129857d7..aa02495c 100644 --- a/core/jvm/src/UtcOffsetJvm.kt +++ b/core/jvm/src/UtcOffsetJvm.kt @@ -6,13 +6,14 @@ package kotlinx.datetime import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.UtcOffsetSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import java.time.DateTimeException import java.time.ZoneOffset import java.time.format.DateTimeFormatterBuilder import java.time.format.* +@Suppress("DEPRECATION") @Serializable(with = UtcOffsetSerializer::class) public actual class UtcOffset(internal val zoneOffset: ZoneOffset) { public actual val totalSeconds: Int get() = zoneOffset.totalSeconds diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 70e0017b..96185694 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -10,7 +10,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* import kotlinx.datetime.internal.* -import kotlinx.datetime.serializers.InstantIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlin.time.* import kotlin.time.Duration.Companion.nanoseconds @@ -38,7 +38,7 @@ private const val MAX_SECOND = 31494816403199L // +1000000-12-31T23:59:59 private fun isValidInstantSecond(second: Long) = second >= MIN_SECOND && second <= MAX_SECOND -@Serializable(with = InstantIso8601Serializer::class) +@Serializable(with = InstantSerializer::class) public actual class Instant internal constructor(public actual val epochSeconds: Long, public actual val nanosecondsOfSecond: Int) : Comparable { init { diff --git a/core/native/src/LocalDate.kt b/core/native/src/LocalDate.kt index 14ee3a17..714bbebf 100644 --- a/core/native/src/LocalDate.kt +++ b/core/native/src/LocalDate.kt @@ -12,7 +12,7 @@ import kotlinx.datetime.format.* import kotlinx.datetime.internal.* import kotlinx.datetime.internal.safeAdd import kotlinx.datetime.internal.safeMultiply -import kotlinx.datetime.serializers.LocalDateIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlin.math.* @@ -22,7 +22,7 @@ internal const val YEAR_MAX = 999_999 private fun isValidYear(year: Int): Boolean = year >= YEAR_MIN && year <= YEAR_MAX -@Serializable(with = LocalDateIso8601Serializer::class) +@Serializable(with = LocalDateSerializer::class) public actual class LocalDate actual constructor(public actual val year: Int, public actual val monthNumber: Int, public actual val dayOfMonth: Int) : Comparable { init { diff --git a/core/native/src/LocalDateTime.kt b/core/native/src/LocalDateTime.kt index 33187d4a..3b58e470 100644 --- a/core/native/src/LocalDateTime.kt +++ b/core/native/src/LocalDateTime.kt @@ -13,7 +13,7 @@ import kotlinx.datetime.internal.* import kotlinx.datetime.serializers.* import kotlinx.serialization.* -@Serializable(with = LocalDateTimeIso8601Serializer::class) +@Serializable(with = LocalDateTimeSerializer::class) public actual class LocalDateTime public actual constructor(public actual val date: LocalDate, public actual val time: LocalTime) : Comparable { public actual companion object { diff --git a/core/native/src/LocalTime.kt b/core/native/src/LocalTime.kt index 28ebd78d..42f5c017 100644 --- a/core/native/src/LocalTime.kt +++ b/core/native/src/LocalTime.kt @@ -10,10 +10,10 @@ package kotlinx.datetime import kotlinx.datetime.internal.* import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.LocalTimeIso8601Serializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable -@Serializable(LocalTimeIso8601Serializer::class) +@Serializable(LocalTimeSerializer::class) public actual class LocalTime actual constructor( public actual val hour: Int, public actual val minute: Int, diff --git a/core/native/src/UtcOffset.kt b/core/native/src/UtcOffset.kt index abe8c64d..6213360b 100644 --- a/core/native/src/UtcOffset.kt +++ b/core/native/src/UtcOffset.kt @@ -7,10 +7,11 @@ package kotlinx.datetime import kotlinx.datetime.internal.* import kotlinx.datetime.format.* -import kotlinx.datetime.serializers.UtcOffsetSerializer +import kotlinx.datetime.serializers.* import kotlinx.serialization.Serializable import kotlin.math.abs +@Suppress("DEPRECATION") @Serializable(with = UtcOffsetSerializer::class) public actual class UtcOffset private constructor(public actual val totalSeconds: Int) { diff --git a/serialization/common/test/DateTimePeriodSerializationTest.kt b/serialization/common/test/DateTimePeriodSerializationTest.kt index a8cb1a6c..9869d424 100644 --- a/serialization/common/test/DateTimePeriodSerializationTest.kt +++ b/serialization/common/test/DateTimePeriodSerializationTest.kt @@ -97,28 +97,34 @@ class DateTimePeriodSerializationTest { @Test fun testDatePeriodIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.DatePeriod ISO", DatePeriodIso8601Serializer) datePeriodIso8601Serialization(DatePeriodIso8601Serializer, DateTimePeriodIso8601Serializer) } @Test fun testDatePeriodComponentSerialization() { + assertKSerializerName("kotlinx.datetime.DatePeriod components", DatePeriodComponentSerializer) datePeriodComponentSerialization(DatePeriodComponentSerializer, DateTimePeriodComponentSerializer) } @Test fun testDateTimePeriodIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.DateTimePeriod ISO", DateTimePeriodIso8601Serializer) dateTimePeriodIso8601Serialization(DateTimePeriodIso8601Serializer) } @Test fun testDateTimePeriodComponentSerialization() { + assertKSerializerName("kotlinx.datetime.DateTimePeriod components", DateTimePeriodComponentSerializer) dateTimePeriodComponentSerialization(DateTimePeriodComponentSerializer) } @Test fun testDefaultSerializers() { // Check that they behave the same as the ISO 8601 serializers + assertKSerializerName("kotlinx.datetime.DateTimePeriod", Json.serializersModule.serializer()) dateTimePeriodIso8601Serialization(Json.serializersModule.serializer()) + assertKSerializerName("kotlinx.datetime.DatePeriod", Json.serializersModule.serializer()) datePeriodIso8601Serialization(Json.serializersModule.serializer(), Json.serializersModule.serializer()) } diff --git a/serialization/common/test/DateTimeUnitSerializationTest.kt b/serialization/common/test/DateTimeUnitSerializationTest.kt index e2b4f8b6..39e57a25 100644 --- a/serialization/common/test/DateTimeUnitSerializationTest.kt +++ b/serialization/common/test/DateTimeUnitSerializationTest.kt @@ -18,7 +18,7 @@ class DateTimeUnitSerializationTest { 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 + val json = "{\"nanoseconds\":$nanoseconds}" assertEquals(json, Json.encodeToString(serializer, unit)) assertEquals(unit, Json.decodeFromString(serializer, json)) } @@ -65,7 +65,7 @@ class DateTimeUnitSerializationTest { repeat(100) { val nanoseconds = Random.nextLong(1, Long.MAX_VALUE) val unit = DateTimeUnit.TimeBased(nanoseconds) - val json = "{\"type\":\"kotlinx.datetime.TimeBased\",\"nanoseconds\":${nanoseconds.toString()}}" // https://youtrack.jetbrains.com/issue/KT-39891 + val json = "{\"type\":\"kotlinx.datetime.TimeBased\",\"nanoseconds\":$nanoseconds}" assertEquals(json, Json.encodeToString(serializer, unit)) assertEquals(unit, Json.decodeFromString(serializer, json)) } @@ -87,21 +87,25 @@ class DateTimeUnitSerializationTest { @Test fun testTimeBasedUnitSerialization() { + assertKSerializerName("kotlinx.datetime.TimeBased", TimeBasedDateTimeUnitSerializer) timeBasedSerialization(TimeBasedDateTimeUnitSerializer) } @Test fun testDayBasedSerialization() { + assertKSerializerName("kotlinx.datetime.DayBased", DayBasedDateTimeUnitSerializer) dayBasedSerialization(DayBasedDateTimeUnitSerializer) } @Test fun testMonthBasedSerialization() { + assertKSerializerName("kotlinx.datetime.MonthBased", MonthBasedDateTimeUnitSerializer) monthBasedSerialization(MonthBasedDateTimeUnitSerializer) } @Test fun testDateBasedSerialization() { + assertKSerializerName("kotlinx.datetime.DateTimeUnit.DateBased", DateBasedDateTimeUnitSerializer) dateBasedSerialization(DateBasedDateTimeUnitSerializer) } diff --git a/serialization/common/test/DayOfWeekSerializationTest.kt b/serialization/common/test/DayOfWeekSerializationTest.kt index aa81e8ac..70eb35a9 100644 --- a/serialization/common/test/DayOfWeekSerializationTest.kt +++ b/serialization/common/test/DayOfWeekSerializationTest.kt @@ -13,10 +13,11 @@ import kotlin.test.* class DayOfWeekSerializationTest { @Test fun testSerialization() { + assertKSerializerName("kotlinx.datetime.DayOfWeek", DayOfWeekSerializer) for (dayOfWeek in DayOfWeek.entries) { 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 index dea5c2db..d7a18a2b 100644 --- a/serialization/common/test/InstantSerializationTest.kt +++ b/serialization/common/test/InstantSerializationTest.kt @@ -5,6 +5,7 @@ package kotlinx.datetime.serialization.test import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeComponents import kotlinx.datetime.serializers.* import kotlinx.serialization.* import kotlinx.serialization.json.* @@ -14,10 +15,26 @@ 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(1607505416, 120000), + "\"2020-12-09T09:16:56.00012Z\""), + Pair(Instant.fromEpochSeconds(-1607505416, -120000), + "\"1919-01-23T14:43:03.99988Z\""), + 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 defaultSerialization(serializer: KSerializer) { + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416, 120000), + "\"2020-12-09T09:16:56.000120Z\""), + Pair(Instant.fromEpochSeconds(-1607505416, -120000), + "\"1919-01-23T14:43:03.999880Z\""), Pair(Instant.fromEpochSeconds(987654321, 123456789), "\"2001-04-19T04:25:21.123456789Z\""), Pair(Instant.fromEpochSeconds(987654321, 0), @@ -53,17 +70,42 @@ class InstantSerializationTest { @Test fun testIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.Instant ISO", InstantIso8601Serializer) iso8601Serialization(InstantIso8601Serializer) } @Test fun testComponentSerialization() { + assertKSerializerName("kotlinx.datetime.Instant components", InstantComponentSerializer) componentSerialization(InstantComponentSerializer) } @Test fun testDefaultSerializers() { // should be the same as the ISO 8601 - iso8601Serialization(Json.serializersModule.serializer()) + assertKSerializerName("kotlinx.datetime.Instant", Json.serializersModule.serializer()) + defaultSerialization(Json.serializersModule.serializer()) + } + + object Rfc1123InstantSerializer : FormattedInstantSerializer("RFC_1123", DateTimeComponents.Formats.RFC_1123) + + @Test + fun testCustomSerializer() { + assertKSerializerName("kotlinx.datetime.Instant serializer RFC_1123", Rfc1123InstantSerializer) + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416), + "\"Wed, 9 Dec 2020 09:16:56 GMT\""), + Pair(Instant.fromEpochSeconds(-1607505416), + "\"Thu, 23 Jan 1919 14:43:04 GMT\""), + Pair(Instant.fromEpochSeconds(987654321), + "\"Thu, 19 Apr 2001 04:25:21 GMT\""), + )) { + assertEquals(json, Json.encodeToString(Rfc1123InstantSerializer, instant)) + assertEquals(instant, Json.decodeFromString(Rfc1123InstantSerializer, json)) + } + assertEquals("\"Thu, 19 Apr 2001 04:25:21 GMT\"", + Json.encodeToString(Rfc1123InstantSerializer, Instant.fromEpochSeconds(987654321, 123456789))) + assertEquals(Instant.fromEpochSeconds(987654321), + Json.decodeFromString(Rfc1123InstantSerializer, "\"Thu, 19 Apr 2001 08:25:21 +0400\"")) } } diff --git a/serialization/common/test/LocalDateSerializationTest.kt b/serialization/common/test/LocalDateSerializationTest.kt index 91ed3827..420c4db4 100644 --- a/serialization/common/test/LocalDateSerializationTest.kt +++ b/serialization/common/test/LocalDateSerializationTest.kt @@ -56,18 +56,40 @@ class LocalDateSerializationTest { @Test fun testIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.LocalDate ISO", LocalDateIso8601Serializer) iso8601Serialization(LocalDateIso8601Serializer) } @Test fun testComponentSerialization() { + assertKSerializerName("kotlinx.datetime.LocalDate components", LocalDateComponentSerializer) componentSerialization(LocalDateComponentSerializer) } @Test fun testDefaultSerializers() { // should be the same as the ISO 8601 + assertKSerializerName("kotlinx.datetime.LocalDate", Json.serializersModule.serializer()) iso8601Serialization(Json.serializersModule.serializer()) } + object IsoBasicLocalDateSerializer : FormattedLocalDateSerializer("ISO_BASIC", LocalDate.Formats.ISO_BASIC) + + @Test + fun testCustomSerializer() { + assertKSerializerName("kotlinx.datetime.LocalDate serializer ISO_BASIC", IsoBasicLocalDateSerializer) + for ((localDate, json) in listOf( + Pair(LocalDate(2020, 12, 9), "\"20201209\""), + Pair(LocalDate(-2020, 1, 1), "\"-20200101\""), + Pair(LocalDate(2019, 10, 1), "\"20191001\""), + )) { + assertEquals(json, Json.encodeToString(IsoBasicLocalDateSerializer, localDate)) + assertEquals(localDate, Json.decodeFromString(IsoBasicLocalDateSerializer, json)) + } + } +} + +@OptIn(ExperimentalSerializationApi::class) +fun assertKSerializerName(expectedName: String, serializer: KSerializer) { + assertEquals(expectedName, serializer.descriptor.serialName) } diff --git a/serialization/common/test/LocalDateTimeSerializationTest.kt b/serialization/common/test/LocalDateTimeSerializationTest.kt index c01e647c..aede7abb 100644 --- a/serialization/common/test/LocalDateTimeSerializationTest.kt +++ b/serialization/common/test/LocalDateTimeSerializationTest.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serialization.test import kotlinx.datetime.* +import kotlinx.datetime.format.char import kotlinx.datetime.serializers.* import kotlinx.serialization.KSerializer import kotlinx.serialization.json.* @@ -14,6 +15,19 @@ 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:00\""), + 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.99\""), + Pair(LocalDateTime(-1, 1, 2, 23, 59, 59, 999990000), "\"-0001-01-02T23:59:59.99999\""), + Pair(LocalDateTime(-2008, 1, 2, 23, 59, 59, 999999990), "\"-2008-01-02T23:59:59.99999999\""), + )) { + assertEquals(json, Json.encodeToString(serializer, localDateTime)) + assertEquals(localDateTime, Json.decodeFromString(serializer, json)) + } + } + + private fun defaultSerialization(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\""), @@ -69,17 +83,43 @@ class LocalDateTimeSerializationTest { @Test fun testIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.LocalDateTime ISO", LocalDateTimeIso8601Serializer) iso8601Serialization(LocalDateTimeIso8601Serializer) } @Test fun testComponentSerialization() { + assertKSerializerName( + "kotlinx.datetime.LocalDateTime components", LocalDateTimeComponentSerializer + ) componentSerialization(LocalDateTimeComponentSerializer) } @Test fun testDefaultSerializers() { // should be the same as the ISO 8601 - iso8601Serialization(Json.serializersModule.serializer()) + assertKSerializerName("kotlinx.datetime.LocalDateTime", Json.serializersModule.serializer()) + defaultSerialization(Json.serializersModule.serializer()) + } + + object PythonDateTimeSerializer : FormattedLocalDateTimeSerializer("PythonDateTime", LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + time(LocalTime.Formats.ISO) + }) + + @Test + fun testCustomSerializer() { + assertKSerializerName("kotlinx.datetime.LocalDateTime serializer PythonDateTime", PythonDateTimeSerializer) + for ((localDateTime, json) in listOf( + Pair(LocalDateTime(2008, 7, 5, 2, 1), "\"2008-07-05 02:01:00\""), + Pair(LocalDateTime(2007, 12, 31, 23, 59, 1), "\"2007-12-31 23:59:01\""), + Pair(LocalDateTime(999, 12, 31, 23, 59, 59, 990000000), "\"0999-12-31 23:59:59.99\""), + Pair(LocalDateTime(-1, 1, 2, 23, 59, 59, 999990000), "\"-0001-01-02 23:59:59.99999\""), + Pair(LocalDateTime(-2008, 1, 2, 23, 59, 59, 999999990), "\"-2008-01-02 23:59:59.99999999\""), + )) { + assertEquals(json, Json.encodeToString(PythonDateTimeSerializer, localDateTime)) + assertEquals(localDateTime, Json.decodeFromString(PythonDateTimeSerializer, json)) + } } } diff --git a/serialization/common/test/LocalTimeSerializationTest.kt b/serialization/common/test/LocalTimeSerializationTest.kt index 5df81f54..c7d44b40 100644 --- a/serialization/common/test/LocalTimeSerializationTest.kt +++ b/serialization/common/test/LocalTimeSerializationTest.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serialization.test import kotlinx.datetime.* +import kotlinx.datetime.format.char import kotlinx.datetime.serializers.* import kotlinx.serialization.KSerializer import kotlinx.serialization.json.* @@ -14,6 +15,19 @@ import kotlin.test.* class LocalTimeSerializationTest { private fun iso8601Serialization(serializer: KSerializer) { + for ((localTime, json) in listOf( + Pair(LocalTime(2, 1), "\"02:01:00\""), + Pair(LocalTime(23, 59, 1), "\"23:59:01\""), + Pair(LocalTime(23, 59, 59, 990000000), "\"23:59:59.99\""), + Pair(LocalTime(23, 59, 59, 999990000), "\"23:59:59.99999\""), + Pair(LocalTime(23, 59, 59, 999999990), "\"23:59:59.99999999\""), + )) { + assertEquals(json, Json.encodeToString(serializer, localTime)) + assertEquals(localTime, Json.decodeFromString(serializer, json)) + } + } + + private fun defaultSerialization(serializer: KSerializer) { for ((localTime, json) in listOf( Pair(LocalTime(2, 1), "\"02:01\""), Pair(LocalTime(23, 59, 1), "\"23:59:01\""), @@ -59,17 +73,39 @@ class LocalTimeSerializationTest { @Test fun testIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.LocalTime ISO", LocalTimeIso8601Serializer) iso8601Serialization(LocalTimeIso8601Serializer) } @Test fun testComponentSerialization() { + assertKSerializerName("kotlinx.datetime.LocalTime components", LocalTimeComponentSerializer) componentSerialization(LocalTimeComponentSerializer) } @Test fun testDefaultSerializers() { - // should be the same as the ISO 8601 - iso8601Serialization(Json.serializersModule.serializer()) + assertKSerializerName("kotlinx.datetime.LocalTime", Json.serializersModule.serializer()) + defaultSerialization(Json.serializersModule.serializer()) + } + + object FixedWidthTimeSerializer : FormattedLocalTimeSerializer("FixedWidth", LocalTime.Format { + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + }) + + @Test + fun testCustomSerializer() { + assertKSerializerName("kotlinx.datetime.LocalTime serializer FixedWidth", FixedWidthTimeSerializer) + for ((localTime, json) in listOf( + Pair(LocalTime(2, 1), "\"02:01:00.000\""), + Pair(LocalTime(23, 59, 1), "\"23:59:01.000\""), + Pair(LocalTime(23, 59, 59, 990000000), "\"23:59:59.990\""), + Pair(LocalTime(23, 59, 59, 999000000), "\"23:59:59.999\""), + )) { + assertEquals(json, Json.encodeToString(FixedWidthTimeSerializer, localTime)) + assertEquals(localTime, Json.decodeFromString(FixedWidthTimeSerializer, json)) + } + assertEquals("\"12:34:56.123\"", Json.encodeToString(FixedWidthTimeSerializer, + LocalTime(12, 34, 56, 123999999))) } } diff --git a/serialization/common/test/MonthSerializationTest.kt b/serialization/common/test/MonthSerializationTest.kt index 3296ba08..70d9b455 100644 --- a/serialization/common/test/MonthSerializationTest.kt +++ b/serialization/common/test/MonthSerializationTest.kt @@ -13,10 +13,11 @@ import kotlin.test.* class MonthSerializationTest { @Test fun testSerialization() { + assertKSerializerName("kotlinx.datetime.Month", MonthSerializer) for (month in Month.entries) { 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 index f4062b0b..b34f5633 100644 --- a/serialization/common/test/TimeZoneSerializationTest.kt +++ b/serialization/common/test/TimeZoneSerializationTest.kt @@ -46,7 +46,11 @@ class TimeZoneSerializationTest { @Test fun testDefaultSerializers() { + assertKSerializerName( + "kotlinx.datetime.FixedOffsetTimeZone", Json.serializersModule.serializer() + ) zoneOffsetSerialization(Json.serializersModule.serializer()) + assertKSerializerName("kotlinx.datetime.TimeZone", Json.serializersModule.serializer()) serialization(Json.serializersModule.serializer()) } -} \ No newline at end of file +} diff --git a/serialization/common/test/UtcOffsetSerializationTest.kt b/serialization/common/test/UtcOffsetSerializationTest.kt index 2504d6fb..b4ac0477 100644 --- a/serialization/common/test/UtcOffsetSerializationTest.kt +++ b/serialization/common/test/UtcOffsetSerializationTest.kt @@ -14,25 +14,66 @@ import kotlin.test.* class UtcOffsetSerializationTest { - private fun testSerializationAsPrimitive(serializer: KSerializer) { - val offset2h = UtcOffset(hours = 2) - assertEquals("\"+02:00\"", Json.encodeToString(serializer, offset2h)) - assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02:00\"")) - assertEquals(offset2h, Json.decodeFromString(serializer, "\"+02:00:00\"")) - - assertFailsWith { - Json.decodeFromString(serializer, "\"UTC+02:00\"") // not an offset + private fun iso8601Serialization(serializer: KSerializer) { + // the default form is obtainable and parsable + for ((offset, json) in listOf( + Pair(UtcOffset(hours = 0), "\"Z\""), + Pair(UtcOffset(hours = 1), "\"+01:00\""), + Pair(UtcOffset(hours = 1, minutes = 30), "\"+01:30\""), + Pair(UtcOffset(hours = 1, minutes = 30, seconds = 59), "\"+01:30:59\""), + )) { + assertEquals(json, Json.encodeToString(serializer, offset)) + assertEquals(offset, Json.decodeFromString(serializer, json)) + } + // alternative forms are also parsable + for ((offset, json) in listOf( + Pair(UtcOffset(hours = 0), "\"+00:00\""), + Pair(UtcOffset(hours = 0), "\"z\""), + )) { + assertEquals(offset, Json.decodeFromString(serializer, json)) + } + // some strings aren't parsable + for (json in listOf( + "\"+3\"", + "\"+03\"", + "\"+03:0\"", + "\"UTC+02:00\"", + )) { + assertFailsWith { + Json.decodeFromString(serializer, json) + } } } @Test - fun defaultSerializer() { - testSerializationAsPrimitive(Json.serializersModule.serializer()) + fun testIso8601Serialization() { + assertKSerializerName("kotlinx.datetime.UtcOffset ISO", UtcOffsetIso8601Serializer) + iso8601Serialization(UtcOffsetIso8601Serializer) } @Test - fun stringPrimitiveSerializer() { - testSerializationAsPrimitive(UtcOffsetSerializer) - testSerializationAsPrimitive(UtcOffset.serializer()) + fun testDefaultSerializers() { + // should be the same as the ISO 8601 + assertKSerializerName("kotlinx.datetime.UtcOffset", Json.serializersModule.serializer()) + iso8601Serialization(Json.serializersModule.serializer()) + assertKSerializerName("kotlinx.datetime.UtcOffset", UtcOffset.serializer()) + iso8601Serialization(UtcOffset.serializer()) + } + + object FourDigitOffsetSerializer : FormattedUtcOffsetSerializer("FOUR_DIGITS", UtcOffset.Formats.FOUR_DIGITS) + + @Test + fun testCustomSerializer() { + assertKSerializerName("kotlinx.datetime.UtcOffset serializer FOUR_DIGITS", FourDigitOffsetSerializer) + for ((utcOffset, json) in listOf( + Pair(UtcOffset.ZERO, "\"+0000\""), + Pair(UtcOffset(2), "\"+0200\""), + Pair(UtcOffset(2, 30), "\"+0230\""), + Pair(UtcOffset(-2, -30), "\"-0230\""), + )) { + assertEquals(json, Json.encodeToString(FourDigitOffsetSerializer, utcOffset)) + assertEquals(utcOffset, Json.decodeFromString(FourDigitOffsetSerializer, json)) + } + assertEquals("\"+1234\"", Json.encodeToString(FourDigitOffsetSerializer, UtcOffset(12, 34, 56))) } }