diff --git a/build.gradle.kts b/build.gradle.kts index c9b8fc2d0..4a5afa4b9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,7 @@ import io.spine.dependency.local.Base import io.spine.dependency.local.CoreJvm import io.spine.dependency.local.Logging import io.spine.dependency.local.Reflect +import io.spine.dependency.local.Time import io.spine.dependency.local.ToolBase import io.spine.dependency.local.Validation import io.spine.gradle.publish.PublishingRepos @@ -86,6 +87,7 @@ buildscript { logging.lib, logging.middleware, validation.runtime, + io.spine.dependency.local.Compiler.api ) } } @@ -101,7 +103,6 @@ plugins { id("org.jetbrains.kotlinx.kover") idea jacoco - `gradle-doctor` `project-report` } @@ -148,6 +149,8 @@ allprojects { Validation.runtime, Validation.javaBundle, CoreJvm.server, + Time.lib, + Time.javaExtensions, ) } } diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index febf8ae9b..304cbf4a9 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -72,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.037" + private const val fallbackVersion = "2.0.0-SNAPSHOT.039" /** * The distinct version of the Compiler used by other build tools. @@ -81,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.037" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.039" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt index a6a93f0ba..49df6fbe1 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvm.kt @@ -39,7 +39,7 @@ typealias CoreJava = CoreJvm @Suppress("ConstPropertyName", "unused") object CoreJvm { const val group = Spine.group - const val version = "2.0.0-SNAPSHOT.370" + const val version = "2.0.0-SNAPSHOT.371" const val coreArtifact = "spine-core" const val clientArtifact = "spine-client" 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 7cf8dd686..1ef4f097a 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.391" + const val version = "2.0.0-SNAPSHOT.402" /** * The last version of Validation compatible with ProtoData. diff --git a/config b/config index dcd2cee3a..17e0dbb81 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit dcd2cee3af82ce8e4de407801636637f7cdcef3c +Subproject commit 17e0dbb819839d9b65b711efb085b38bcbb5eae9 diff --git a/dependencies.md b/dependencies.md index 1da917c4b..dae2d4947 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-time:2.0.0-SNAPSHOT.231` +# Dependencies of `io.spine:spine-time:2.0.0-SNAPSHOT.232` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1006,14 +1006,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Dec 26 17:25:17 WET 2025** using +This report was generated on **Tue Mar 10 19:57:35 WET 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.231` +# Dependencies of `io.spine:spine-time-java:2.0.0-SNAPSHOT.232` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1849,14 +1849,14 @@ This report was generated on **Fri Dec 26 17:25:17 WET 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Dec 26 17:25:17 WET 2025** using +This report was generated on **Tue Mar 10 19:57:35 WET 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.231` +# Dependencies of `io.spine:spine-time-kotlin:2.0.0-SNAPSHOT.232` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2704,14 +2704,14 @@ This report was generated on **Fri Dec 26 17:25:17 WET 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Dec 26 17:25:17 WET 2025** using +This report was generated on **Tue Mar 10 19:57:35 WET 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:spine-time-testlib:2.0.0-SNAPSHOT.231` +# Dependencies of `io.spine.tools:spine-time-testlib:2.0.0-SNAPSHOT.232` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -3547,6 +3547,6 @@ This report was generated on **Fri Dec 26 17:25:17 WET 2025** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Dec 26 17:25:17 WET 2025** using +This report was generated on **Tue Mar 10 19:57:35 WET 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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f8e1ee312..61285a659 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2b5..dbc3ce4a0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/pom.xml b/pom.xml index 35febca6e..d5696a46a 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.231 +2.0.0-SNAPSHOT.232 2015 @@ -56,7 +56,7 @@ all modules and does not describe the project structure per-subproject. io.spine spine-validation-jvm-runtime - 2.0.0-SNAPSHOT.391 + 2.0.0-SNAPSHOT.402 compile @@ -193,12 +193,12 @@ all modules and does not describe the project structure per-subproject. io.spine.tools compiler-cli-all - 2.0.0-SNAPSHOT.037 + 2.0.0-SNAPSHOT.039 io.spine.tools compiler-protoc-plugin - 2.0.0-SNAPSHOT.037 + 2.0.0-SNAPSHOT.039 io.spine.tools @@ -218,7 +218,7 @@ all modules and does not describe the project structure per-subproject. io.spine.tools validation-java-bundle - 2.0.0-SNAPSHOT.391 + 2.0.0-SNAPSHOT.402 net.sourceforge.pmd diff --git a/time/src/main/java/io/spine/time/validation/When.java b/time/src/main/java/io/spine/time/validation/When.java deleted file mode 100644 index c3e85ba87..000000000 --- a/time/src/main/java/io/spine/time/validation/When.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2025, 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.time.validation; - -import com.google.protobuf.Timestamp; -import io.spine.code.proto.FieldContext; -import io.spine.validation.Constraint; -import io.spine.validation.option.FieldValidatingOption; - -/** - * A validating option that specified the point in time which - * a {@link Timestamp} field value has. - */ -final class When extends FieldValidatingOption { - - private When() { - super(TimeOptionsProto.when); - } - - /** Creates a new instance of this option. */ - public static When create() { - return new When(); - } - - @Override - public Constraint constraintFor(FieldContext value) { - return new WhenConstraint(optionValue(value), value.targetDeclaration()); - } -} diff --git a/time/src/main/java/io/spine/time/validation/WhenConstraint.java b/time/src/main/java/io/spine/time/validation/WhenConstraint.java deleted file mode 100644 index dfc2d8917..000000000 --- a/time/src/main/java/io/spine/time/validation/WhenConstraint.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2025, 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.time.validation; - -import com.google.common.collect.ImmutableList; -import com.google.protobuf.Message; -import com.google.protobuf.Timestamp; -import io.spine.code.proto.FieldContext; -import io.spine.code.proto.FieldDeclaration; -import io.spine.time.Temporal; -import io.spine.time.Temporals; -import io.spine.validation.ConstraintViolation; -import io.spine.validation.CustomConstraint; -import io.spine.validation.FieldValue; -import io.spine.validation.MessageValue; -import io.spine.validation.TemplateString; -import io.spine.validation.TemplateStrings; -import io.spine.validation.diags.ViolationText; -import io.spine.validation.option.FieldConstraint; - -import java.util.Locale; - -import static io.spine.time.validation.Time.FUTURE; -import static io.spine.time.validation.Time.TIME_UNDEFINED; - -/** - * A constraint that, when applied to a {@link Timestamp} field value, checks for whether the - * actual value is in the future or in the past, defined by the value of the field option. - */ -final class WhenConstraint extends FieldConstraint implements CustomConstraint { - - /** - * The name of the placeholder for reporting the value of the {@link TimeOption#getIn in} - * constraint on a temporal field. - */ - private static final String TIME_PLACEHOLDER = "field.time"; - - WhenConstraint(TimeOption optionValue, FieldDeclaration field) { - super(optionValue, field); - } - - @Override - public ImmutableList validate(MessageValue message) { - var fieldValue = message.valueOf(field()); - var when = optionValue().getIn(); - if (when == TIME_UNDEFINED) { - return ImmutableList.of(); - } - var violations = - fieldValue.values() - .map(msg -> Temporals.from((Message) msg)) - .filter(temporalValue -> isTimeInvalid(temporalValue, when)) - .findFirst() - .map(invalidValue -> ImmutableList.of( - newTimeViolation(fieldValue, invalidValue) - )) - .orElse(ImmutableList.of()); - return violations; - } - - @Override - public TemplateString errorMessage(FieldContext field) { - var option = optionValue(); - var errorMsg = ViolationText.errorMessage(option, option.getErrorMsg()); - var time = option.getIn().name().toLowerCase(Locale.ENGLISH); - var builder = TemplateString.newBuilder() - .setWithPlaceholders(errorMsg) - .putPlaceholderValue(TIME_PLACEHOLDER, time); - TemplateStrings.withField(builder, field.targetDeclaration()); - return builder.build(); - } - - @Override - public String formattedErrorMessage(FieldContext field) { - var templateString = errorMessage(field); - return TemplateStrings.format(templateString); - } - - /** - * Checks the time. - * - * @param temporalValue - * a time point to check - * @param whenExpected - * the time when the checked timestamp should be - * @return {@code true} if the time is valid according to {@code whenExpected} parameter, - * {@code false} otherwise - */ - private static boolean isTimeInvalid(Temporal temporalValue, Time whenExpected) { - var valid = (whenExpected == FUTURE) - ? temporalValue.isInFuture() - : temporalValue.isInPast(); - return !valid; - } - - private ConstraintViolation newTimeViolation(FieldValue fieldValue, Temporal value) { - var msg = errorMessage(fieldValue.context()); - var fieldPath = fieldValue.context().fieldPath(); - var violation = ConstraintViolation.newBuilder() - .setMessage(msg) - .setFieldPath(fieldPath) - .setFieldValue(value.packed()) - .build(); - return violation; - } -} diff --git a/time/src/main/java/io/spine/time/validation/WhenFactory.kt b/time/src/main/java/io/spine/time/validation/WhenFactory.kt deleted file mode 100644 index 00fee6deb..000000000 --- a/time/src/main/java/io/spine/time/validation/WhenFactory.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2025, 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.time.validation - -import com.google.auto.service.AutoService -import com.google.common.collect.ImmutableSet -import com.google.errorprone.annotations.Immutable -import io.spine.annotation.Internal -import io.spine.validation.option.FieldValidatingOption -import io.spine.validation.option.ValidatingOptionFactory - -/** - * An implementation of [ValidatingOptionFactory] which adds the [When] option - * for message fields. - */ -@AutoService(ValidatingOptionFactory::class) -@Internal -@Immutable -public class WhenFactory : ValidatingOptionFactory { - - override fun forMessage(): Set> { - return typeOptions - } - - private companion object { - - private val typeOptions by lazy { - ImmutableSet.of>(When.create()) - } - } -} diff --git a/time/src/main/kotlin/io/spine/time/validation/LocalDateValidator.kt b/time/src/main/kotlin/io/spine/time/validation/LocalDateValidator.kt new file mode 100644 index 000000000..5a3fb83c1 --- /dev/null +++ b/time/src/main/kotlin/io/spine/time/validation/LocalDateValidator.kt @@ -0,0 +1,98 @@ +/* + * 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.time.validation + +import com.google.auto.service.AutoService +import io.spine.base.FieldPath +import io.spine.time.LocalDate +import io.spine.time.Month +import io.spine.validation.DetectedViolation +import io.spine.validation.FieldViolation +import io.spine.validation.MessageValidator +import io.spine.validation.RuntimeErrorPlaceholder.FIELD_PATH +import io.spine.validation.RuntimeErrorPlaceholder.RANGE_VALUE +import io.spine.validation.templateString +import java.time.Year +import java.time.YearMonth + +/** + * Validates [LocalDate] messages. + * + * Ensures that the day of a month is within the range allowed for the given month. + * This takes into account the number of days in February in leap years. + */ +@AutoService(MessageValidator::class) +public class LocalDateValidator : MessageValidator { + + @Suppress("ReturnCount") + override fun validate(message: LocalDate): List { + val year = message.year + val month = message.month + val day = message.day + + if (month == Month.MONTH_UNDEFINED || month == Month.UNRECOGNIZED) { + // There is nothing we can do in such a situation because `Month` is an enum. + // We do not restrict enum field values because it does not have much sense + // from the domain language point of view. + return emptyList() + } + + if (year < Year.MIN_VALUE || year > Year.MAX_VALUE) { + // This is a safety net for the `YearMonth.of()` call which fails + // when the year is out of range defined by Java Time. + // We return an empty list because we have an option-based constraint + // on the `year` field for these values, and validation will fail in the generated code. + // We do not want to duplicate the error message for the `year` being out of range. + return emptyList() + } + + val daysInMonth = YearMonth.of(year, month.number).lengthOfMonth() + if (day > daysInMonth) { + return listOf(invalidDay(day, daysInMonth)) + } + return emptyList() + } +} + +/** + * Creates a violation for an invalid day of the month. + * + * @param day the invalid day value. + * @param maxDays the maximum allowed days for the month. + */ +private fun invalidDay(day: Int, maxDays: Int): FieldViolation = FieldViolation( + message = templateString { + withPlaceholders = "The \${$FIELD_PATH} value is out of range" + + " (\${$RANGE_VALUE}): $day." + placeholderValue.put(FIELD_PATH.value, "day") + placeholderValue.put(RANGE_VALUE.value, "1..$maxDays") + }, + fieldPath = FieldPath.newBuilder() + .addFieldName("day") + .build(), + fieldValue = day +) diff --git a/time/src/main/proto/spine/time/time.proto b/time/src/main/proto/spine/time/time.proto index 7da7156ea..17c7bd157 100644 --- a/time/src/main/proto/spine/time/time.proto +++ b/time/src/main/proto/spine/time/time.proto @@ -66,7 +66,7 @@ message YearMonth { }; // A year with `1` being year 1 CE and `-1` being 1 BC. - int32 year = 1; + int32 year = 1 [(min).value = "-999999999", (max).value = "999999999"]; // One of 12 Gregorian calendar months specified by `Month`. Month month = 2 [(required) = true]; @@ -102,12 +102,16 @@ message LocalDate { }; // A year with `1` being year 1 CE and `-1` being 1 BC. - int32 year = 1; + int32 year = 1 [(min).value = "-999999999", (max).value = "999999999"]; // One of 12 Gregorian calendar months specified by `Month`. Month month = 2 [(required) = true]; // A day which must be from 1 to 31 and valid for the year and month. + // + // In generated code for Kotlin/Java this is checked by + // `io.spine.time.validation.LocalDateValidator`. + // int32 day = 3 [(min).value = "1", (max).value = "31"]; } @@ -150,8 +154,8 @@ message LocalDateTime { field: "time" }; - LocalDate date = 1 [(required) = true, (validate) = true]; - LocalTime time = 2 [(validate) = true]; + LocalDate date = 1 [(required) = true]; + LocalTime time = 2; } // A time-zone offset from UTC, such as `+02:00`. @@ -171,7 +175,7 @@ message OffsetTime { option deprecated = true; // The local time. - LocalTime time = 1 [(validate) = true]; + LocalTime time = 1; // The offset of the time-zone from UTC. ZoneOffset offset = 2; @@ -187,7 +191,7 @@ message OffsetDateTime { option (is).java_type = "OffsetDateTimeTemporal"; // The local date-time. - LocalDateTime date_time = 1 [(required) = true, (validate) = true]; + LocalDateTime date_time = 1 [(required) = true]; // The offset of the time-zone from UTC. ZoneOffset offset = 2; @@ -210,7 +214,7 @@ message ZonedDateTime { option (is).java_type = "ZonedDateTimeTemporal"; // The local date-time. - LocalDateTime date_time = 1 [(required) = true, (validate) = true]; + LocalDateTime date_time = 1 [(required) = true]; // The time-zone. ZoneId zone = 2 [(required) = true]; diff --git a/time/src/test/java/io/spine/time/validation/WhenFactoryTest.java b/time/src/test/java/io/spine/time/validation/WhenFactoryTest.java deleted file mode 100644 index 157cfab0a..000000000 --- a/time/src/test/java/io/spine/time/validation/WhenFactoryTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2025, 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.time.validation; - -import com.google.common.collect.Iterables; -import io.spine.validation.option.ValidatingOptionFactory; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.ServiceLoader; - -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.truth.Truth.assertThat; - -@DisplayName("`WhenFactory` should") -class WhenFactoryTest { - - @Test - @DisplayName("be discoverable to `ServiceLoader`") - void beLoaded() { - var loader = ServiceLoader.load(ValidatingOptionFactory.class); - List optionFactories = newArrayList(loader); - assertThat(optionFactories.size()).isAtLeast(1); - var timeOptionFactory = optionFactories.stream() - .filter(WhenFactory.class::isInstance) - .findAny(); - assertThat(timeOptionFactory).isPresent(); - } - - @Test - @DisplayName("declare (when) option") - void declareWhen() { - ValidatingOptionFactory factory = new WhenFactory(); - var messageOptions = factory.forMessage(); - assertThat(messageOptions).hasSize(1); - var option = Iterables.getOnlyElement(messageOptions); - assertThat(option).isInstanceOf(When.class); - } -} diff --git a/time/src/test/kotlin/io/spine/time/LocalDateSpec.kt b/time/src/test/kotlin/io/spine/time/LocalDateSpec.kt new file mode 100644 index 000000000..a880ec0f5 --- /dev/null +++ b/time/src/test/kotlin/io/spine/time/LocalDateSpec.kt @@ -0,0 +1,100 @@ +/* + * 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.time + +import io.kotest.matchers.optional.shouldBePresent +import io.kotest.matchers.shouldBe +import io.spine.protobuf.TypeConverter +import io.spine.validation.ConstraintViolation +import java.time.Year.MAX_VALUE +import java.time.Year.MIN_VALUE +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +@DisplayName("`LocalDate` should") +internal class LocalDateSpec { + + @Nested + @DisplayName("validate `year` within range") + inner class YearRange { + + @ParameterizedTest + @ValueSource(ints = [MIN_VALUE, 0, 2024, MAX_VALUE]) + fun `allow valid year values`(yearValue: Int) { + assertDoesNotThrow { + localDate { + year = yearValue + month = Month.JANUARY + day = 1 + } + } + } + + @Test + fun `detect year value below the minimum`() { + val yearValue = MIN_VALUE - 1 + val date = LocalDate.newBuilder() + .setYear(yearValue) + .setMonth(Month.JANUARY) + .setDay(1) + .buildPartial() + val error = date.validate() + error.shouldBePresent() + val violation = error.get().constraintViolationList[0] + violation.matches(yearValue, "min.value", MIN_VALUE.toString()) + } + + @Test + fun `detect year value above the maximum`() { + val yearValue = MAX_VALUE + 1 + val date = LocalDate.newBuilder() + .setYear(yearValue) + .setMonth(Month.JANUARY) + .setDay(1) + .buildPartial() + val error = date.validate() + error.shouldBePresent() + val violation = error.get().constraintViolationList[0] + violation.matches(yearValue, "max.value", MAX_VALUE.toString()) + } + } + + private fun ConstraintViolation.matches( + yearValue: Int, + limitKey: String, + limitValue: String + ) { + fieldPath.getFieldName(0) shouldBe "year" + val value = TypeConverter.toObject(fieldValue, Int::class.javaObjectType) + value shouldBe yearValue + message.placeholderValueMap[limitKey] shouldBe limitValue + } +} diff --git a/time/src/test/kotlin/io/spine/time/validation/LocalDateValidatorSpec.kt b/time/src/test/kotlin/io/spine/time/validation/LocalDateValidatorSpec.kt new file mode 100644 index 000000000..9f82a71bf --- /dev/null +++ b/time/src/test/kotlin/io/spine/time/validation/LocalDateValidatorSpec.kt @@ -0,0 +1,135 @@ +/* + * 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.time.validation + +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.spine.time.LocalDate +import io.spine.time.Month +import io.spine.time.localDate +import io.spine.validation.FieldViolation +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +@DisplayName("`LocalDateValidator` should") +internal class LocalDateValidatorSpec { + + private val validator = LocalDateValidator() + + @Test + fun `allow valid dates`() { + assertDoesNotThrow { + localDate { + year = 2026 + month = Month.JANUARY + day = 31 + } + } + } + + @ParameterizedTest + @DisplayName("detect invalid day for a month") + @CsvSource( + "2024, APRIL, 31, 30", + "2024, JUNE, 31, 30", + "2024, SEPTEMBER, 31, 30", + "2024, NOVEMBER, 31, 30", + "2023, FEBRUARY, 29, 28", + "2024, FEBRUARY, 30, 29" + ) + fun detectInvalidDay(year: Int, month: Month, day: Int, maxDays: Int) { + val date = LocalDate.newBuilder() + .setYear(year) + .setMonth(month) + .setDay(day) + .buildPartial() + val violations = validator.validate(date) + violations shouldHaveSize 1 + val violation = violations[0] as FieldViolation + violation.run { + fieldPath!!.fieldNameList[0] shouldBe "day" + fieldValue shouldBe day + message.withPlaceholders shouldBe + "The \${field.path} value is out of range (\${range.value}): $day." + message.placeholderValueMap["range.value"] shouldBe "1..$maxDays" + } + } + + @Nested + @DisplayName("handle leap years and") + inner class LeapYear { + + @Test + fun `allow Feb 29 in a leap year`() { + val date = LocalDate.newBuilder() + .setYear(2024) + .setMonth(Month.FEBRUARY) + .setDay(29) + .buildPartial() + validator.validate(date).shouldBeEmpty() + } + + @Test + fun `detect invalid Feb 29 in a non-leap year`() { + val date = LocalDate.newBuilder() + .setYear(2023) + .setMonth(Month.FEBRUARY) + .setDay(29) + .buildPartial() + val violations = validator.validate(date) + violations shouldHaveSize 1 + val violation = violations[0] as FieldViolation + violation.message.placeholderValueMap["range.value"] shouldBe "1..28" + } + } + + /** + * The test verifies that if a month is not defined the `LocalDate` instance + * is considered valid. + * + * There is nothing we can do in such a situation because `Month` is an enum. + * We do not restrict enum field values because it does not have much sense + * from the domain language point of view. + * + * We still want `MONTH_UNDEFINED` item to support the "unset" notion for a month + * as we have such a thing for other enums in the code. + */ + @Test + fun `ignore undefined month`() { + val date = LocalDate.newBuilder() + .setYear(2024) + .setMonth(Month.MONTH_UNDEFINED) + .setDay(31) + .buildPartial() + validator.validate(date).shouldBeEmpty() + } +} diff --git a/version.gradle.kts b/version.gradle.kts index 4772111ca..398a68f00 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -29,4 +29,4 @@ * * For dependencies on Spine modules please see [io.spine.dependency.local.Spine]. */ -val versionToPublish by extra("2.0.0-SNAPSHOT.231") +val versionToPublish by extra("2.0.0-SNAPSHOT.232")