From 53fd1833685f8d44098e2746289c71d2558e4778 Mon Sep 17 00:00:00 2001 From: Oleg-Melnik Date: Thu, 23 Jul 2026 01:58:38 +0300 Subject: [PATCH 1/4] Handle out-of-range date-time values. --- .../spine/chords/proto/time/DateTimeField.kt | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt b/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt index 8777db69..f9990fbc 100644 --- a/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt +++ b/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025, TeamDev. All rights reserved. + * Copyright 2026, TeamDev. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,10 +59,10 @@ import io.spine.chords.proto.value.time.DefaultDatePattern import java.time.Instant import java.time.LocalDateTime import java.time.OffsetDateTime +import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter.ofPattern import java.time.format.DateTimeParseException -import java.util.* private const val DefaultDateTimeFormat = "$DefaultDatePattern HH:mm" @@ -108,18 +108,31 @@ public class DateTimeField : InputField() { ) } - override fun parseValue(rawText: String): Timestamp { - val offsetDateTime = try { - LocalDateTime.parse( - complementWithPattern(rawText, dateTimePattern).string, - ofPattern(dateTimePattern) - ) - } catch (e: DateTimeParseException) { - throw ParseException("Enter a valid value", e) - } - val millis = Date.from(offsetDateTime.toInstant(WallClock.zoneOffset)).time - return Timestamps.fromMillis(millis) + override fun parseValue(rawText: String): Timestamp = + parseDateTime(rawText, dateTimePattern, WallClock.zoneOffset) +} + +internal fun parseDateTime( + rawText: String, + dateTimePattern: DateTimePattern, + zoneOffset: ZoneOffset +): Timestamp { + val localDateTime = try { + LocalDateTime.parse( + complementWithPattern(rawText, dateTimePattern).string, + ofPattern(dateTimePattern) + ) + } catch (e: DateTimeParseException) { + throw ParseException("Enter a valid value", e) + } + val instant = localDateTime.toInstant(zoneOffset) + if (!Timestamps.isValid(instant.epochSecond, instant.nano)) { + throw ParseException("Enter a date/time within the supported range") } + return Timestamp.newBuilder() + .setSeconds(instant.epochSecond) + .setNanos(instant.nano) + .build() } /** From 142699d2baa2abb2c32bd019314a6c64416a525a Mon Sep 17 00:00:00 2001 From: Oleg-Melnik Date: Thu, 23 Jul 2026 01:58:43 +0300 Subject: [PATCH 2/4] Cover date-time range boundaries. --- .../chords/proto/time/DateTimeFieldSpec.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 proto/src/test/kotlin/io/spine/chords/proto/time/DateTimeFieldSpec.kt diff --git a/proto/src/test/kotlin/io/spine/chords/proto/time/DateTimeFieldSpec.kt b/proto/src/test/kotlin/io/spine/chords/proto/time/DateTimeFieldSpec.kt new file mode 100644 index 00000000..989010bc --- /dev/null +++ b/proto/src/test/kotlin/io/spine/chords/proto/time/DateTimeFieldSpec.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.chords.proto.time + +import com.google.protobuf.Timestamp +import com.google.protobuf.util.Timestamps +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.DisplayName +import io.kotest.matchers.shouldBe +import io.spine.chords.core.ParseException +import java.time.Instant +import java.time.ZoneOffset +import org.junit.jupiter.api.Test + +private const val TestDateTimePattern = "yyyy-MM-dd HH:mm" +private const val OutOfRangeMessage = "Enter a date/time within the supported range" + +@DisplayName("`DateTimeField` should") +internal class DateTimeFieldSpec { + + @Test + fun `reject a local date-time before the Protobuf range`() { + val exception = shouldThrow { + parseDateTime("000101010000", TestDateTimePattern, ZoneOffset.ofHours(2)) + } + + exception.validationErrorMessage shouldBe OutOfRangeMessage + } + + @Test + fun `accept the minimum Protobuf timestamp in a positive offset`() { + val result = parseDateTime( + "000101010200", + TestDateTimePattern, + ZoneOffset.ofHours(2) + ) + + result shouldBe Timestamps.MIN_VALUE + } + + @Test + fun `reject a local date-time after the Protobuf range`() { + val exception = shouldThrow { + parseDateTime("999912312359", TestDateTimePattern, ZoneOffset.ofHours(-2)) + } + + exception.validationErrorMessage shouldBe OutOfRangeMessage + } + + @Test + fun `parse an ordinary local date-time`() { + val result = parseDateTime("202507011230", TestDateTimePattern, ZoneOffset.UTC) + val expected = Timestamp.newBuilder() + .setSeconds(Instant.parse("2025-07-01T12:30:00Z").epochSecond) + .build() + + result shouldBe expected + } +} From 430d1343d0689f248fd88721779069648e5678a9 Mon Sep 17 00:00:00 2001 From: Oleg-Melnik Date: Thu, 23 Jul 2026 01:58:49 +0300 Subject: [PATCH 3/4] =?UTF-8?q?Bump=20version=20=E2=80=94>=20`2.0.0-SNAPSH?= =?UTF-8?q?OT.92`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dependencies.md | 24 ++++++++++++------------ pom.xml | 2 +- version.gradle.kts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/dependencies.md b/dependencies.md index 1d2fd29e..41b22394 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.91` +# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.92` ## Runtime 1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.**No license information found** @@ -1104,12 +1104,12 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jul 22 22:13:08 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 23 01:48:20 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.91` +# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.92` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1899,12 +1899,12 @@ This report was generated on **Wed Jul 22 22:13:08 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jul 22 22:13:09 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 23 01:48:21 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.91` +# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.92` ## Runtime 1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1. @@ -2938,12 +2938,12 @@ This report was generated on **Wed Jul 22 22:13:09 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jul 22 22:13:10 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 23 01:48:22 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.91` +# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.92` ## Runtime 1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.**No license information found** @@ -3976,12 +3976,12 @@ This report was generated on **Wed Jul 22 22:13:10 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jul 22 22:13:11 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 23 01:48:23 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.91` +# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.92` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4775,12 +4775,12 @@ This report was generated on **Wed Jul 22 22:13:11 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jul 22 22:13:12 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu Jul 23 01:48:23 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.91` +# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.92` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5544,4 +5544,4 @@ This report was generated on **Wed Jul 22 22:13:12 EEST 2026** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Jul 22 22:13:13 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file +This report was generated on **Thu Jul 23 01:48:24 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8dfdd118..43ddd8ab 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.chords Chords -2.0.0-SNAPSHOT.91 +2.0.0-SNAPSHOT.92 2015 diff --git a/version.gradle.kts b/version.gradle.kts index 05f1a3b3..8cdd77d0 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of all Chords libraries. */ -val chordsVersion: String by extra("2.0.0-SNAPSHOT.91") +val chordsVersion: String by extra("2.0.0-SNAPSHOT.92") From 14908fbd1b08cc5fa833bbd18f277bcfd3d00308 Mon Sep 17 00:00:00 2001 From: Oleg-Melnik Date: Thu, 23 Jul 2026 02:06:10 +0300 Subject: [PATCH 4/4] Document date-time parsing. --- .../spine/chords/proto/time/DateTimeField.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt b/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt index f9990fbc..833c61f7 100644 --- a/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt +++ b/proto/src/main/kotlin/io/spine/chords/proto/time/DateTimeField.kt @@ -112,6 +112,24 @@ public class DateTimeField : InputField() { parseDateTime(rawText, dateTimePattern, WallClock.zoneOffset) } +/** + * Parses date and time from raw input field text into a Protobuf [Timestamp]. + * + * The [rawText] contains only editable characters of the date/time value. The + * separators specified by [dateTimePattern] are restored before parsing. The + * resulting local date/time is interpreted at [zoneOffset]. + * + * @param rawText + * the editable characters entered into the input field. + * @param dateTimePattern + * the pattern used to restore separators and parse the date/time. + * @param zoneOffset + * the offset used to convert the local date/time into an instant. + * @return the parsed date/time represented as a Protobuf timestamp. + * @throws ParseException + * if the text cannot be parsed or the resulting instant is outside + * the range supported by Protobuf timestamps. + */ internal fun parseDateTime( rawText: String, dateTimePattern: DateTimePattern,