diff --git a/.agents/_TOC.md b/.agents/_TOC.md
index 065df133a..83375f417 100644
--- a/.agents/_TOC.md
+++ b/.agents/_TOC.md
@@ -13,4 +13,4 @@
11. [Advanced safety rules](advanced-safety-rules.md)
12. [Refactoring guidelines](refactoring-guidelines.md)
13. [Common tasks](common-tasks.md)
-14. [Java to Kotlin conversion](java-kotlin-conversion.md)
+14. [Java to Kotlin conversion](skills/java-to-kotlin/SKILL.md)
diff --git a/.agents/quick-reference-card.md b/.agents/quick-reference-card.md
index 6c25b9a7f..e2be69cb8 100644
--- a/.agents/quick-reference-card.md
+++ b/.agents/quick-reference-card.md
@@ -3,7 +3,6 @@
```
🔑 Key Information:
- Kotlin/Java project with CQRS architecture
-- Use ChatGPT for documentation, Codex for code generation, GPT-4o for complex analysis
- Follow coding guidelines in Spine Event Engine docs
- Always include tests with code changes
- Version bump required for all PRs
diff --git a/.agents/java-kotlin-conversion.md b/.agents/skills/java-to-kotlin/SKILL.md
similarity index 88%
rename from .agents/java-kotlin-conversion.md
rename to .agents/skills/java-to-kotlin/SKILL.md
index 95cf92954..d3abdc2f7 100644
--- a/.agents/java-kotlin-conversion.md
+++ b/.agents/skills/java-to-kotlin/SKILL.md
@@ -1,3 +1,11 @@
+---
+name: java-to-kotlin
+description: >
+ Convert Java code to Kotlin, including Java API comments from Javadoc to KDoc.
+ Use when asked to migrate Java files, classes, methods, nullability semantics,
+ or common Java patterns into idiomatic Kotlin while preserving behavior.
+---
+
# 🪄 Converting Java code to Kotlin
* Java code API comments are Javadoc format.
diff --git a/.agents/skills/java-to-kotlin/agents/openai.yaml b/.agents/skills/java-to-kotlin/agents/openai.yaml
new file mode 100644
index 000000000..252920fed
--- /dev/null
+++ b/.agents/skills/java-to-kotlin/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "Java to Kotlin"
+ short_description: "Convert Java code to idiomatic Kotlin"
+ default_prompt: "Use $java-to-kotlin to convert Java code to Kotlin while preserving behavior, nullability, and API documentation wording."
diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml
index f8c24933f..b571ce82f 100644
--- a/.github/workflows/build-on-ubuntu.yml
+++ b/.github/workflows/build-on-ubuntu.yml
@@ -1,10 +1,9 @@
-name: Build under Ubuntu
+name: Build on Ubuntu
on: push
jobs:
build:
- name: Build under Ubuntu
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml
index 4e6b57f1f..37eba1be7 100644
--- a/.github/workflows/build-on-windows.yml
+++ b/.github/workflows/build-on-windows.yml
@@ -1,10 +1,9 @@
-name: Build under Windows
+name: Build on Windows
on: pull_request
jobs:
build:
- name: Build under Windows
runs-on: windows-latest
steps:
diff --git a/.github/workflows/ensure-reports-updated.yml b/.github/workflows/ensure-reports-updated.yml
index fdd8b8e67..93531059e 100644
--- a/.github/workflows/ensure-reports-updated.yml
+++ b/.github/workflows/ensure-reports-updated.yml
@@ -8,8 +8,7 @@ on:
- '**'
jobs:
- build:
- name: Ensure license reports updated
+ check:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index 858cebbcc..a7cde85e5 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -9,7 +9,6 @@ on:
jobs:
validation:
- name: Gradle Wrapper Validation
runs-on: ubuntu-latest
steps:
- name: Checkout latest code
diff --git a/.github/workflows/increment-guard.yml b/.github/workflows/increment-guard.yml
index 1993841a6..dd7df7137 100644
--- a/.github/workflows/increment-guard.yml
+++ b/.github/workflows/increment-guard.yml
@@ -9,8 +9,7 @@ on:
- '**'
jobs:
- build:
- name: Check version increment
+ check:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml
index fe8ad849d..f70617100 100644
--- a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml
+++ b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml
@@ -34,7 +34,7 @@ env:
jobs:
retrieve-package-names:
- name: Retrieve the package names published from this repository
+ name: Retrieve package names
runs-on: ubuntu-latest
outputs:
package-names: ${{ steps.request-package-names.outputs.package-names }}
@@ -54,7 +54,7 @@ jobs:
echo "package-names=$(<./package-names.json)" >> $GITHUB_OUTPUT
delete-obsolete-artifacts:
- name: Remove obsolete artifacts published from this repository to GitHub Packages
+ name: Delete obsolete artifacts
needs: retrieve-package-names
runs-on: ubuntu-latest
strategy:
diff --git a/.gitignore b/.gitignore
index a3e0ec13b..5f85d295e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@
# Internal tool directories.
.fleet/
+.junie/memory/
# Kotlin temp directories.
**/.kotlin/
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
index 459e28eca..5160f499e 100644
--- a/.junie/guidelines.md
+++ b/.junie/guidelines.md
@@ -10,7 +10,7 @@ Also follow the Junie-specific rules described below.
## Junie Assistance Tips
-When working with Junie AI on the Spine Tool-Base project:
+When working with Junie AI on the Spine family of projects:
1. **Project Navigation**: Use `search_project` to find relevant files and code segments.
2. **Code Understanding**: Request file structure with `get_file_structure` before editing.
diff --git a/.junie/skills b/.junie/skills
new file mode 120000
index 000000000..2b7a412b8
--- /dev/null
+++ b/.junie/skills
@@ -0,0 +1 @@
+../.agents/skills
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 6496c646c..4184e9bf2 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -137,8 +137,6 @@ val koverVersion = "0.9.1"
/**
* The version of the Shadow Plugin.
*
- * `7.1.2` is the last version compatible with Gradle 7.x. Newer versions require Gradle v8.x.
- *
* @see Shadow Plugin releases
*/
val shadowVersion = "9.4.1"
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt
index 858731b3f..6b4822dcd 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt
@@ -26,7 +26,6 @@
package io.spine.dependency.build
-import io.spine.dependency.build.Dokka.GradlePlugin.id
import io.spine.dependency.local.Spine
// https://github.com/Kotlin/dokka
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt
index dfdaf9535..2333e4ce7 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt
@@ -46,7 +46,7 @@ object CoreJvmCompiler {
/**
* The version used to in the build classpath.
*/
- const val dogfoodingVersion = "2.0.0-SNAPSHOT.062"
+ const val dogfoodingVersion = "2.0.0-SNAPSHOT.063"
/**
* The version to be used for integration tests.
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt
index 275aa0070..05eb7bfaf 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt
@@ -40,7 +40,7 @@ import io.spine.dependency.Dependency
)
object Time : Dependency() {
override val group = Spine.group
- override val version = "2.0.0-SNAPSHOT.235"
+ override val version = "2.0.0-SNAPSHOT.236"
private const val infix = "spine-time"
fun lib(version: String): String = "$group:$infix:$version"
diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
index a5ef40318..121d7aac2 100644
--- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
+++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt
@@ -36,7 +36,7 @@ object Validation {
/**
* The version of the Validation library artifacts.
*/
- const val version = "2.0.0-SNAPSHOT.413"
+ const val version = "2.0.0-SNAPSHOT.414"
/**
* The last version of Validation compatible with ProtoData.
diff --git a/config b/config
index cd6e86330..f24236f0a 160000
--- a/config
+++ b/config
@@ -1 +1 @@
-Subproject commit cd6e86330c283e0414be7c318a4cd450dd3c6982
+Subproject commit f24236f0a897e9d6a101e60d1dd936317c7a3c06
diff --git a/dependencies.md b/dependencies.md
index 843bea5ab..df15203e7 100644
--- a/dependencies.md
+++ b/dependencies.md
@@ -1,6 +1,6 @@
-# Dependencies of `io.spine.tools:time-gradle-plugin:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine.tools:time-gradle-plugin:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -1059,14 +1059,14 @@
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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.tools:time-testlib:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine.tools:time-testlib:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -1869,14 +1869,14 @@ This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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:spine-time:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine:spine-time:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -2833,14 +2833,14 @@ This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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:spine-time-java:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine:spine-time-java:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -3643,14 +3643,14 @@ This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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:spine-time-kotlin:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine:spine-time-kotlin:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -4461,14 +4461,14 @@ This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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.tools:time-validation:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine.tools:time-validation:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -5590,14 +5590,14 @@ This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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:spine-validation-tests:2.0.0-SNAPSHOT.236`
+# Dependencies of `io.spine:spine-validation-tests:2.0.0-SNAPSHOT.237`
## Runtime
1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0.
@@ -6683,6 +6683,6 @@ This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Apr 28 13:46:33 WEST 2026** using
+This report was generated on **Fri May 01 17:48:31 WEST 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 64877507f..8ba7a58fe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject.
-->
io.spine
spine-time
-2.0.0-SNAPSHOT.236
+2.0.0-SNAPSHOT.237
2015
@@ -53,10 +53,16 @@ all modules and does not describe the project structure per-subproject.
2.0.0-SNAPSHOT.387
compile
+
+ io.spine
+ spine-time
+ 2.0.0-SNAPSHOT.236
+ compile
+
io.spine
spine-validation-jvm-runtime
- 2.0.0-SNAPSHOT.413
+ 2.0.0-SNAPSHOT.414
compile
@@ -239,7 +245,7 @@ all modules and does not describe the project structure per-subproject.
io.spine.tools
core-jvm-plugins
- 2.0.0-SNAPSHOT.062
+ 2.0.0-SNAPSHOT.063
io.spine.tools
@@ -251,10 +257,15 @@ all modules and does not describe the project structure per-subproject.
spine-dokka-extensions
2.0.0-SNAPSHOT.7
+
+ io.spine.tools
+ time-validation
+ 2.0.0-SNAPSHOT.236
+
io.spine.tools
validation-java-bundle
- 2.0.0-SNAPSHOT.413
+ 2.0.0-SNAPSHOT.414
net.sourceforge.pmd
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/Fixtures.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/Fixtures.kt
new file mode 100644
index 000000000..c6b304dfb
--- /dev/null
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/Fixtures.kt
@@ -0,0 +1,73 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.tools.time.validation.java
+
+import com.google.protobuf.Timestamp
+import com.google.protobuf.util.Durations.fromMillis
+import com.google.protobuf.util.Timestamps
+import io.spine.time.LocalDateTimes
+import io.spine.time.LocalDateTime
+import java.time.Instant
+import java.time.LocalDateTime.ofInstant
+import java.time.ZoneOffset.UTC
+
+/**
+ * Five hundred milliseconds.
+ *
+ * To shift the time into the past or future, we add or subtract a difference of this amount.
+ *
+ * There are two reasons for choosing 500 milliseconds:
+ *
+ * 1. The generated code uses `io.spine.base.Time.currentTime()` to get the current timestamp
+ * for comparison. In turn, this method relies on `io.spine.base.Time.SystemTimeProvider`
+ * by default, which has millisecond precision.
+ * 2. Adding too small amount of time to make the stamp denote "future" might be unreliable.
+ * As it could catch up `now` by the time `Time.currentTime()` is invoked.
+ */
+private const val HALF_OF_SECOND: Long = 500
+
+internal object TemporalFixtures {
+
+ fun pastTime(): LocalDateTime {
+ val current = Instant.now() // It is a UTC stamp.
+ return LocalDateTimes.of(ofInstant(current.minusMillis(HALF_OF_SECOND), UTC))
+ }
+
+ fun futureTime(): LocalDateTime {
+ val current = Instant.now() // It is a UTC stamp.
+ return LocalDateTimes.of(ofInstant(current.plusMillis(HALF_OF_SECOND), UTC))
+ }
+}
+
+internal object TimestampFixtures {
+
+ fun pastTime(): Timestamp =
+ Timestamps.subtract(Timestamps.now(), fromMillis(HALF_OF_SECOND))
+
+ fun futureTime(): Timestamp =
+ Timestamps.add(Timestamps.now(), fromMillis(HALF_OF_SECOND))
+}
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/SpineTemporalWhenSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/SpineTemporalWhenSpec.kt
deleted file mode 100644
index 5f4115c37..000000000
--- a/tests/src/test/kotlin/io/spine/tools/time/validation/java/SpineTemporalWhenSpec.kt
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * 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, must retain the above copyright notice and the following
- * disclaimer.
- *
- * 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.tools.time.validation.java
-
-import io.spine.test.tools.validate.anySpineTemporal
-import io.spine.test.tools.validate.anySpineTemporals
-import io.spine.test.tools.validate.futureSpineTemporal
-import io.spine.test.tools.validate.futureSpineTemporals
-import io.spine.test.tools.validate.pastSpineTemporal
-import io.spine.test.tools.validate.pastSpineTemporals
-import io.spine.time.LocalDateTimes
-import java.time.Instant
-import java.time.LocalDateTime.ofInstant
-import java.time.ZoneOffset.UTC
-import org.junit.jupiter.api.DisplayName
-import org.junit.jupiter.api.Nested
-import org.junit.jupiter.api.Test
-import io.spine.time.LocalDateTime as SpineTimeLocalDateTime
-
-@DisplayName("If used with Spine `Temporal`, `(when)` constraint should")
-internal class SpineTemporalWhenSpec {
-
- @Nested inner class
- `when given a temporal denoting` {
-
- @Nested inner class
- `the past` {
-
- @Test
- fun `throw, if restricted to be in future`() = assertValidationFails {
- futureSpineTemporal {
- value = pastTime()
- }
- }
-
- @Test
- fun `pass, if restricted to be in past`() = assertValidationPasses {
- pastSpineTemporal {
- value = pastTime()
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anySpineTemporal {
- value = pastTime()
- }
- }
- }
-
- @Nested inner class
- `the future` {
-
- @Test
- fun `throw, if restricted to be in past`() = assertValidationFails {
- pastSpineTemporal {
- value = futureTime()
- }
- }
-
- @Test
- fun `pass, if restricted to be in future`() = assertValidationPasses {
- futureSpineTemporal {
- value = futureTime()
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anySpineTemporal {
- value = futureTime()
- }
- }
- }
- }
-
- @Nested inner class
- `when given several times` {
-
- @Nested inner class
- `denoting only the past` {
-
- private val severalPastTimes = listOf(pastTime(), pastTime(), pastTime())
-
- @Test
- fun `throw, if restricted to be in future`() = assertValidationFails {
- futureSpineTemporals {
- value.addAll(severalPastTimes)
- }
- }
-
- @Test
- fun `pass, if restricted to be in past`() = assertValidationPasses {
- pastSpineTemporals {
- value.addAll(severalPastTimes)
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anySpineTemporals {
- value.addAll(severalPastTimes)
- }
- }
- }
-
- @Nested inner class
- `denoting only the future` {
-
- private val severalFutureTimes = listOf(futureTime(), futureTime(), futureTime())
-
- @Test
- fun `throw, if restricted to be in past`() = assertValidationFails {
- pastSpineTemporals {
- value.addAll(severalFutureTimes)
- }
- }
-
- @Test
- fun `pass, if restricted to be in future`() = assertValidationPasses {
- futureSpineTemporals {
- value.addAll(severalFutureTimes)
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anySpineTemporals {
- value.addAll(severalFutureTimes)
- }
- }
- }
-
- @Nested inner class
- `with a single past time within the future times` {
-
- private val severalFutureAndPast = listOf(futureTime(), pastTime(), futureTime())
-
- @Test
- fun `throw, if restricted to be in future`() = assertValidationFails {
- futureSpineTemporals {
- value.addAll(severalFutureAndPast)
- }
- }
-
- @Test
- fun `throw, if restricted to be in past`() = assertValidationFails {
- pastSpineTemporals {
- value.addAll(severalFutureAndPast)
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anySpineTemporals {
- value.addAll(severalFutureAndPast)
- }
- }
- }
-
- @Nested inner class
- `with a single future time within the past times` {
-
- private val severalPastAndFuture = listOf(pastTime(), futureTime(), pastTime())
-
- @Test
- fun `throw, if restricted to be in future`() = assertValidationFails {
- futureSpineTemporals {
- value.addAll(severalPastAndFuture)
- }
- }
-
- @Test
- fun `throw, if restricted to be in past`() = assertValidationFails {
- pastSpineTemporals {
- value.addAll(severalPastAndFuture)
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anySpineTemporals {
- value.addAll(severalPastAndFuture)
- }
- }
- }
- }
-}
-
-private fun pastTime(): SpineTimeLocalDateTime {
- val current = Instant.now() // It is a UTC stamp.
- val past = current.minusMillis(HALF_OF_SECOND)
- return LocalDateTimes.of(ofInstant(past, UTC))
-}
-
-private fun futureTime(): SpineTimeLocalDateTime {
- val current = Instant.now() // It is a UTC stamp.
- val past = current.plusMillis(HALF_OF_SECOND)
- return LocalDateTimes.of(ofInstant(past, UTC))
-}
-
-/**
- * Five hundred milliseconds.
- *
- * To shift the time into the past or future, we add or subtract a difference of this amount.
- *
- * There are two reasons for choosing 500 milliseconds:
- *
- * 1. The generated code uses `io.spine.base.Time.currentTime()` to get the current timestamp
- * for comparison. In turn, this method relies on `io.spine.base.Time.SystemTimeProvider`
- * by default, which has millisecond precision.
- * 2. Adding too small amount of time to make the stamp denote "future" might be unreliable.
- * As it could catch up `now` by the time `Time.currentTime()` is invoked.
- */
-private const val HALF_OF_SECOND: Long = 500
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalRepeatedWhenSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalRepeatedWhenSpec.kt
new file mode 100644
index 000000000..83e55b15c
--- /dev/null
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalRepeatedWhenSpec.kt
@@ -0,0 +1,148 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.tools.time.validation.java
+
+import io.spine.test.tools.validate.anySpineTemporals
+import io.spine.test.tools.validate.futureSpineTemporals
+import io.spine.test.tools.validate.pastSpineTemporals
+import io.spine.tools.time.validation.java.TemporalFixtures.futureTime
+import io.spine.tools.time.validation.java.TemporalFixtures.pastTime
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+@DisplayName("If used with repeated `Temporal`, `(when)` constraint should")
+internal class TemporalRepeatedWhenSpec {
+
+ @Nested inner class
+ `denoting only the past` {
+
+ private val severalPastTimes = listOf(pastTime(), pastTime(), pastTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureSpineTemporals {
+ value.addAll(severalPastTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in past`() = assertValidationPasses {
+ pastSpineTemporals {
+ value.addAll(severalPastTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporals {
+ value.addAll(severalPastTimes)
+ }
+ }
+ }
+
+ @Nested inner class
+ `denoting only the future` {
+
+ private val severalFutureTimes = listOf(futureTime(), futureTime(), futureTime())
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastSpineTemporals {
+ value.addAll(severalFutureTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in future`() = assertValidationPasses {
+ futureSpineTemporals {
+ value.addAll(severalFutureTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporals {
+ value.addAll(severalFutureTimes)
+ }
+ }
+ }
+
+ @Nested inner class
+ `with a single past time within the future times` {
+
+ private val severalFutureAndPast = listOf(futureTime(), pastTime(), futureTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureSpineTemporals {
+ value.addAll(severalFutureAndPast)
+ }
+ }
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastSpineTemporals {
+ value.addAll(severalFutureAndPast)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporals {
+ value.addAll(severalFutureAndPast)
+ }
+ }
+ }
+
+ @Nested inner class
+ `with a single future time within the past times` {
+
+ private val severalPastAndFuture = listOf(pastTime(), futureTime(), pastTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureSpineTemporals {
+ value.addAll(severalPastAndFuture)
+ }
+ }
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastSpineTemporals {
+ value.addAll(severalPastAndFuture)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporals {
+ value.addAll(severalPastAndFuture)
+ }
+ }
+ }
+}
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalWhenMapSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalWhenMapSpec.kt
new file mode 100644
index 000000000..e695479d6
--- /dev/null
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalWhenMapSpec.kt
@@ -0,0 +1,121 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.tools.time.validation.java
+
+import io.spine.test.tools.validate.anySpineTemporalMap
+import io.spine.test.tools.validate.futureSpineTemporalMap
+import io.spine.test.tools.validate.pastSpineTemporalMap
+import io.spine.tools.time.validation.java.TemporalFixtures.futureTime
+import io.spine.tools.time.validation.java.TemporalFixtures.pastTime
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+@DisplayName("If used with a map of `Temporal` values, `(when)` constraint should")
+internal class TemporalWhenMapSpec {
+
+ @Nested inner class
+ `only the past` {
+
+ private val severalPastTimes = mapOf("a" to pastTime(), "b" to pastTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureSpineTemporalMap {
+ value.putAll(severalPastTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in past`() = assertValidationPasses {
+ pastSpineTemporalMap {
+ value.putAll(severalPastTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporalMap {
+ value.putAll(severalPastTimes)
+ }
+ }
+ }
+
+ @Nested inner class
+ `only the future` {
+
+ private val severalFutureTimes = mapOf("a" to futureTime(), "b" to futureTime())
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastSpineTemporalMap {
+ value.putAll(severalFutureTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in future`() = assertValidationPasses {
+ futureSpineTemporalMap {
+ value.putAll(severalFutureTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporalMap {
+ value.putAll(severalFutureTimes)
+ }
+ }
+ }
+
+ @Nested inner class
+ `a mix of past and future` {
+
+ private val mixedTimes = mapOf("a" to futureTime(), "b" to pastTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureSpineTemporalMap {
+ value.putAll(mixedTimes)
+ }
+ }
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastSpineTemporalMap {
+ value.putAll(mixedTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporalMap {
+ value.putAll(mixedTimes)
+ }
+ }
+ }
+}
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalWhenSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalWhenSpec.kt
new file mode 100644
index 000000000..8ef9ed260
--- /dev/null
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TemporalWhenSpec.kt
@@ -0,0 +1,90 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.tools.time.validation.java
+
+import io.spine.test.tools.validate.anySpineTemporal
+import io.spine.test.tools.validate.futureSpineTemporal
+import io.spine.test.tools.validate.pastSpineTemporal
+import io.spine.tools.time.validation.java.TemporalFixtures.futureTime
+import io.spine.tools.time.validation.java.TemporalFixtures.pastTime
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+@DisplayName("If used with `Temporal`, `(when)` constraint should")
+internal class TemporalWhenSpec {
+
+ @Nested inner class
+ `the past` {
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureSpineTemporal {
+ value = pastTime()
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in past`() = assertValidationPasses {
+ pastSpineTemporal {
+ value = pastTime()
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporal {
+ value = pastTime()
+ }
+ }
+ }
+
+ @Nested inner class
+ `the future` {
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastSpineTemporal {
+ value = futureTime()
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in future`() = assertValidationPasses {
+ futureSpineTemporal {
+ value = futureTime()
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anySpineTemporal {
+ value = futureTime()
+ }
+ }
+ }
+}
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/ProtoTimestampWhenSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampRepeatedWhenSpec.kt
similarity index 63%
rename from tests/src/test/kotlin/io/spine/tools/time/validation/java/ProtoTimestampWhenSpec.kt
rename to tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampRepeatedWhenSpec.kt
index b124f1423..9954a946e 100644
--- a/tests/src/test/kotlin/io/spine/tools/time/validation/java/ProtoTimestampWhenSpec.kt
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampRepeatedWhenSpec.kt
@@ -26,76 +26,17 @@
package io.spine.tools.time.validation.java
-import com.google.protobuf.Duration
-import com.google.protobuf.Timestamp
-import com.google.protobuf.util.Durations.fromMillis
-import com.google.protobuf.util.Timestamps
-import io.spine.test.tools.validate.anyProtoTimestamp
import io.spine.test.tools.validate.anyProtoTimestamps
-import io.spine.test.tools.validate.futureProtoTimestamp
import io.spine.test.tools.validate.futureProtoTimestamps
-import io.spine.test.tools.validate.pastProtoTimestamp
import io.spine.test.tools.validate.pastProtoTimestamps
+import io.spine.tools.time.validation.java.TimestampFixtures.futureTime
+import io.spine.tools.time.validation.java.TimestampFixtures.pastTime
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
-@DisplayName("If used with Protobuf `Timestamp`, `(when)` constrain should")
-internal class ProtoTimestampWhenSpec {
-
- @Nested inner class
- `when given a timestamp denoting` {
-
- @Nested inner class
- `the past` {
-
- @Test
- fun `throw, if restricted to be in future`() = assertValidationFails {
- futureProtoTimestamp {
- value = pastTime()
- }
- }
-
- @Test
- fun `pass, if restricted to be in past`() = assertValidationPasses {
- pastProtoTimestamp {
- value = pastTime()
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anyProtoTimestamp {
- value = pastTime()
- }
- }
- }
-
- @Nested inner class
- `the future` {
-
- @Test
- fun `throw, if restricted to be in past`() = assertValidationFails {
- pastProtoTimestamp {
- value = futureTime()
- }
- }
-
- @Test
- fun `pass, if restricted to be in future`() = assertValidationPasses {
- futureProtoTimestamp {
- value = futureTime()
- }
- }
-
- @Test
- fun `pass, if not restricted at all`() = assertValidationPasses {
- anyProtoTimestamp {
- value = futureTime()
- }
- }
- }
- }
+@DisplayName("If used with repeated Protobuf `Timestamp`, `(when)` constraint should")
+internal class TimestampRepeatedWhenSpec {
@Nested inner class
`when given several timestamps` {
@@ -209,30 +150,3 @@ internal class ProtoTimestampWhenSpec {
}
}
}
-
-private fun pastTime(): Timestamp {
- val current = Timestamps.now()
- val past = Timestamps.subtract(current, HALF_OF_SECONDS)
- return past
-}
-
-private fun futureTime(): Timestamp {
- val current = Timestamps.now()
- val future = Timestamps.add(current, HALF_OF_SECONDS)
- return future
-}
-
-/**
- * Protobuf [Duration] of five hundred milliseconds.
- *
- * To shift the time into the past or future, we add or subtract a difference of this amount.
- *
- * There are two reasons for choosing 500 milliseconds:
- *
- * 1. The generated code uses `io.spine.base.Time.currentTime()` to get the current timestamp
- * for comparison. In turn, this method relies on `io.spine.base.Time.SystemTimeProvider`
- * by default, which has millisecond precision.
- * 2. Adding too small amount of time to make the stamp denote "future" might be unreliable.
- * As it could catch up `now` by the time `Time.currentTime()` is invoked.
- */
-private val HALF_OF_SECONDS: Duration = fromMillis(500)
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampWhenMapSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampWhenMapSpec.kt
new file mode 100644
index 000000000..630b65b17
--- /dev/null
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampWhenMapSpec.kt
@@ -0,0 +1,125 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.tools.time.validation.java
+
+import io.spine.test.tools.validate.anyProtoTimestampMap
+import io.spine.test.tools.validate.futureProtoTimestampMap
+import io.spine.test.tools.validate.pastProtoTimestampMap
+import io.spine.tools.time.validation.java.TimestampFixtures.futureTime
+import io.spine.tools.time.validation.java.TimestampFixtures.pastTime
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+@DisplayName("If used with a map of Protobuf `Timestamp` values, `(when)` constraint should")
+internal class TimestampWhenMapSpec {
+
+ @Nested inner class
+ `when given a map with timestamps denoting` {
+
+ @Nested inner class
+ `only the past` {
+
+ private val severalPastTimes = mapOf("a" to pastTime(), "b" to pastTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureProtoTimestampMap {
+ value.putAll(severalPastTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in past`() = assertValidationPasses {
+ pastProtoTimestampMap {
+ value.putAll(severalPastTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anyProtoTimestampMap {
+ value.putAll(severalPastTimes)
+ }
+ }
+ }
+
+ @Nested inner class
+ `only the future` {
+
+ private val severalFutureTimes = mapOf("a" to futureTime(), "b" to futureTime())
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastProtoTimestampMap {
+ value.putAll(severalFutureTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in future`() = assertValidationPasses {
+ futureProtoTimestampMap {
+ value.putAll(severalFutureTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anyProtoTimestampMap {
+ value.putAll(severalFutureTimes)
+ }
+ }
+ }
+
+ @Nested inner class
+ `a mix of past and future` {
+
+ private val mixedTimes = mapOf("a" to futureTime(), "b" to pastTime())
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureProtoTimestampMap {
+ value.putAll(mixedTimes)
+ }
+ }
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastProtoTimestampMap {
+ value.putAll(mixedTimes)
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anyProtoTimestampMap {
+ value.putAll(mixedTimes)
+ }
+ }
+ }
+ }
+}
diff --git a/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampWhenSpec.kt b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampWhenSpec.kt
new file mode 100644
index 000000000..1547c6f74
--- /dev/null
+++ b/tests/src/test/kotlin/io/spine/tools/time/validation/java/TimestampWhenSpec.kt
@@ -0,0 +1,94 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.tools.time.validation.java
+
+import io.spine.test.tools.validate.anyProtoTimestamp
+import io.spine.test.tools.validate.futureProtoTimestamp
+import io.spine.test.tools.validate.pastProtoTimestamp
+import io.spine.tools.time.validation.java.TimestampFixtures.futureTime
+import io.spine.tools.time.validation.java.TimestampFixtures.pastTime
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+
+@DisplayName("If used with Protobuf `Timestamp`, `(when)` constraint should")
+internal class TimestampWhenSpec {
+
+ @Nested inner class
+ `when given a timestamp denoting` {
+
+ @Nested inner class
+ `the past` {
+
+ @Test
+ fun `throw, if restricted to be in future`() = assertValidationFails {
+ futureProtoTimestamp {
+ value = pastTime()
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in past`() = assertValidationPasses {
+ pastProtoTimestamp {
+ value = pastTime()
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anyProtoTimestamp {
+ value = pastTime()
+ }
+ }
+ }
+
+ @Nested inner class
+ `the future` {
+
+ @Test
+ fun `throw, if restricted to be in past`() = assertValidationFails {
+ pastProtoTimestamp {
+ value = futureTime()
+ }
+ }
+
+ @Test
+ fun `pass, if restricted to be in future`() = assertValidationPasses {
+ futureProtoTimestamp {
+ value = futureTime()
+ }
+ }
+
+ @Test
+ fun `pass, if not restricted at all`() = assertValidationPasses {
+ anyProtoTimestamp {
+ value = futureTime()
+ }
+ }
+ }
+ }
+}
diff --git a/tests/src/testFixtures/proto/spine/test/tools/validate/when_map.proto b/tests/src/testFixtures/proto/spine/test/tools/validate/when_map.proto
new file mode 100644
index 000000000..0ce4d24bb
--- /dev/null
+++ b/tests/src/testFixtures/proto/spine/test/tools/validate/when_map.proto
@@ -0,0 +1,69 @@
+/*
+ * 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, must retain the above copyright notice and the following
+ * disclaimer.
+ *
+ * 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.
+ */
+syntax = "proto3";
+
+package spine.test.tools.validate;
+
+import "spine/options.proto";
+import "spine/time_options.proto";
+
+option (type_url_prefix) = "type.spine.io";
+option java_package = "io.spine.test.tools.validate";
+option java_outer_classname = "WhenMapProto";
+option java_multiple_files = true;
+
+import "google/protobuf/timestamp.proto";
+import "spine/time/time.proto";
+
+// Tests `PAST` restriction with a map of Protobuf timestamps.
+message PastProtoTimestampMap {
+ map value = 1 [(when).in = PAST];
+}
+
+// Tests `PAST` restriction with a map of Spine temporals.
+message PastSpineTemporalMap {
+ map value = 1 [(when).in = PAST];
+}
+
+// Tests `FUTURE` restriction with a map of Protobuf timestamps.
+message FutureProtoTimestampMap {
+ map value = 1 [(when).in = FUTURE];
+}
+
+// Tests `FUTURE` restriction with a map of Spine temporals.
+message FutureSpineTemporalMap {
+ map value = 1 [(when).in = FUTURE];
+}
+
+// Tests that a map of Protobuf timestamps is not restricted when there's no option.
+message AnyProtoTimestampMap {
+ map value = 1;
+}
+
+// Tests that a map of Spine temporals is not restricted when there's no option.
+message AnySpineTemporalMap {
+ map value = 1;
+}
diff --git a/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenGenerator.kt b/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenGenerator.kt
index 34ef9f9d0..da910ad11 100644
--- a/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenGenerator.kt
+++ b/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenGenerator.kt
@@ -30,6 +30,7 @@ import io.spine.base.FieldPath
import io.spine.server.query.select
import io.spine.time.validation.Time.FUTURE
import io.spine.tools.compiler.ast.TypeName
+import io.spine.tools.compiler.ast.isMap
import io.spine.tools.compiler.ast.isRepeatedMessage
import io.spine.tools.compiler.ast.name
import io.spine.tools.compiler.jvm.CodeBlock
@@ -113,6 +114,15 @@ private class GenerateWhen(
""".trimIndent()
)
+ fieldType.isMap ->
+ CodeBlock(
+ """
+ for (var element : $fieldValue.values()) {
+ ${validateTime(ReadVar("element"))}
+ }
+ """.trimIndent()
+ )
+
else -> unsupportedFieldType()
}.run { SingleOptionCode(this) }
diff --git a/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenOption.kt b/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenOption.kt
index 4b267736f..eed6573a5 100644
--- a/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenOption.kt
+++ b/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenOption.kt
@@ -47,6 +47,7 @@ import io.spine.tools.compiler.ast.FieldType
import io.spine.tools.compiler.ast.File
import io.spine.tools.compiler.ast.event.FieldOptionDiscovered
import io.spine.tools.compiler.ast.extractMessageType
+import io.spine.tools.compiler.ast.isMap
import io.spine.tools.compiler.ast.isRepeatedMessage
import io.spine.tools.compiler.ast.name
import io.spine.tools.compiler.ast.qualifiedName
@@ -155,13 +156,13 @@ private fun checkFieldType(field: Field, typeSystem: TypeSystem, file: File): Ti
}
/**
- * Analysis the given [fieldType], determining whether it represents
+ * Analyses the given [fieldType], determining whether it represents
* the Protobuf [Timestamp] or Spine [Temporal].
*
* For other field types, the method returns [TimeFieldType.TFT_UNKNOWN].
*/
private fun TypeSystem.determineTimeType(fieldType: FieldType): TimeFieldType {
- if (!fieldType.isMessage && !fieldType.isRepeatedMessage) {
+ if (!fieldType.isMessage && !fieldType.isRepeatedMessage && !fieldType.isMap) {
return TFT_UNKNOWN
}
val messageType = fieldType.extractMessageType(typeSystem = this)?.name
diff --git a/version.gradle.kts b/version.gradle.kts
index 1df9dd0b8..388e257dc 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -27,4 +27,4 @@
/**
* The version of this library for publishing.
*/
-val versionToPublish by extra("2.0.0-SNAPSHOT.236")
+val versionToPublish by extra("2.0.0-SNAPSHOT.237")