From 58494590863fe740d99ac36bd75f1a3dcf9c5c47 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 14:19:26 +0200 Subject: [PATCH 01/19] Check `(goes)` for unsupported placeholders --- .../kotlin/io/spine/validation/GoesOption.kt | 20 ++++++++ .../io/spine/validation/GoesPolicySpec.kt | 20 ++++++-- .../spine/validation/goes_option_spec.proto | 48 +++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 model/src/testFixtures/proto/spine/validation/goes_option_spec.proto diff --git a/model/src/main/kotlin/io/spine/validation/GoesOption.kt b/model/src/main/kotlin/io/spine/validation/GoesOption.kt index 1f72278409..306aa54357 100644 --- a/model/src/main/kotlin/io/spine/validation/GoesOption.kt +++ b/model/src/main/kotlin/io/spine/validation/GoesOption.kt @@ -53,6 +53,11 @@ import io.spine.server.entity.alter import io.spine.server.event.Just import io.spine.server.event.React import io.spine.server.event.just +import io.spine.validation.ErrorPlaceholder.FIELD_PATH +import io.spine.validation.ErrorPlaceholder.FIELD_TYPE +import io.spine.validation.ErrorPlaceholder.FIELD_VALUE +import io.spine.validation.ErrorPlaceholder.GOES_COMPANION +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE import io.spine.validation.event.GoesFieldDiscovered import io.spine.validation.event.goesFieldDiscovered @@ -66,6 +71,7 @@ import io.spine.validation.event.goesFieldDiscovered * 2. The companion field is present in the message. * 3. The companion field and the target field are different fields. * 4. The companion field type is supported by the option. + * 5. The error message does not contain unsupported placeholders. * * Any violation of the above conditions leads to a compilation error. */ @@ -90,6 +96,8 @@ internal class GoesPolicy : Policy() { checkCompanionType(companionField, file) val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + checkPlaceholders(message, field, file) + return goesFieldDiscovered { id = field.ref errorMessage = message @@ -138,6 +146,18 @@ private fun checkFieldsDistinct(field: Field, companion: Field, file: File) = " The invalid field: `${field.qualifiedName}`." } +private fun checkPlaceholders(template: String, field: Field, file: File) { + val missing = missingPlaceholders(template, SUPPORTED_PLACEHOLDERS) + Compilation.check(missing.isEmpty(), file, field.span) { + "The `${field.qualifiedName}` field specifies a custom error message for the `($GOES)`" + + " option using unsupported placeholders: `$missing`. Supported placeholders are" + + " the following: `${SUPPORTED_PLACEHOLDERS.map { it.value }}`." + } +} + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, GOES_COMPANION) + /** * Tells if this [FieldType] can be validated with the `(goes)` option. */ diff --git a/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt index d53695f385..f577ff96d0 100644 --- a/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt @@ -29,6 +29,7 @@ package io.spine.validation import com.google.protobuf.Descriptors.Descriptor import com.google.protobuf.Message import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldInclude import io.spine.protodata.ast.Field import io.spine.protodata.ast.name import io.spine.protodata.ast.qualifiedName @@ -40,12 +41,12 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource -@DisplayName("`GoesPolicy` should reject the option") +@DisplayName("`GoesPolicy` should reject the option when") internal class GoesPolicySpec : CompilationErrorTest() { @MethodSource("io.spine.validation.GoesPolicyTestEnv#messagesWithUnsupportedTarget") @ParameterizedTest(name = "when target field type is `{0}`") - fun whenTargetFieldHasUnsupportedType(message: KClass) { + fun targetFieldHasUnsupportedType(message: KClass) { val descriptor = message.descriptor val error = assertCompilationFails(descriptor) val field = descriptor.field("target") @@ -54,7 +55,7 @@ internal class GoesPolicySpec : CompilationErrorTest() { @MethodSource("io.spine.validation.GoesPolicyTestEnv#messagesWithUnsupportedCompanion") @ParameterizedTest(name = "when companion's field type is `{0}`") - fun whenCompanionFieldHasUnsupportedType(message: KClass) { + fun companionFieldHasUnsupportedType(message: KClass) { val descriptor = message.descriptor val error = assertCompilationFails(descriptor) val field = descriptor.field("companion") @@ -76,6 +77,19 @@ internal class GoesPolicySpec : CompilationErrorTest() { val field = message.field("name") error.message shouldContain selfCompanion(field) } + + @Test + fun `when the error message contains unsupported placeholders`() { + val message = GoesWithInvalidPlaceholders.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.qualifiedName) + shouldContain(GOES) + shouldContain("unsupported placeholders") + shouldInclude("[field.name]") + } + } } private fun unsupportedFieldType(field: Field) = diff --git a/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto b/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto new file mode 100644 index 0000000000..abf780ecb3 --- /dev/null +++ b/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto @@ -0,0 +1,48 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package spine.validation; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.validation"; +option java_outer_classname = "GoesOptionSpecProto"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; + +// Provides a `(goes)` field that specifies a custom error message using +// the placeholders not supported by the option. +message GoesWithInvalidPlaceholders { + string companion = 1; + string value = 2 [ + (.goes).with = "companion", + (goes).error_msg = "The `${field.name}` field does not have a value." + ]; +} From 0ac4f38ff5ea4d7285ceac4fdb3e04d82199e8a2 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 14:20:48 +0200 Subject: [PATCH 02/19] Remove a typo --- model/src/main/kotlin/io/spine/validation/ChoiceOption.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index 87d6cf269e..97351e5684 100644 --- a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt @@ -1,5 +1,5 @@ /* -` * Copyright 2024, TeamDev. All rights reserved. + * Copyright 2024, 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. From 743d423c2d95a0a2770f38d4aa93e8b634d0306e Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 14:56:42 +0200 Subject: [PATCH 03/19] Introduce a common `checkPlaceholders()` implementation --- dependencies.md | 28 +++---- .../io/spine/validation/DistinctOption.kt | 12 +-- .../io/spine/validation/ErrorPlaceholder.kt | 22 ----- .../io/spine/validation/ErrorPlaceholders.kt | 80 +++++++++++++++++++ .../kotlin/io/spine/validation/GoesOption.kt | 17 +--- .../io/spine/validation/SetOnceOption.kt | 12 +-- .../validation/required/RequiredOption.kt | 13 +-- .../io/spine/validation/DistinctOptionSpec.kt | 2 +- .../io/spine/validation/RequiredOptionSpec.kt | 2 +- .../io/spine/validation/SetOnceOptionSpec.kt | 2 +- 10 files changed, 105 insertions(+), 85 deletions(-) create mode 100644 model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt diff --git a/dependencies.md b/dependencies.md index 3e4519d35b..57a70922ca 100644 --- a/dependencies.md +++ b/dependencies.md @@ -861,7 +861,7 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:48:59 CEST 2025** 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 **Mon Apr 28 14:55:27 CEST 2025** 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). @@ -1466,7 +1466,7 @@ This report was generated on **Mon Apr 28 12:48:59 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:48:59 CEST 2025** 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 **Mon Apr 28 14:55:27 CEST 2025** 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). @@ -2154,7 +2154,7 @@ This report was generated on **Mon Apr 28 12:48:59 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:48:59 CEST 2025** 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 **Mon Apr 28 14:55:27 CEST 2025** 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). @@ -3128,7 +3128,7 @@ This report was generated on **Mon Apr 28 12:48:59 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:00 CEST 2025** 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 **Mon Apr 28 14:55:28 CEST 2025** 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). @@ -4018,7 +4018,7 @@ This report was generated on **Mon Apr 28 12:49:00 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:00 CEST 2025** 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 **Mon Apr 28 14:55:28 CEST 2025** 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). @@ -4972,7 +4972,7 @@ This report was generated on **Mon Apr 28 12:49:00 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:00 CEST 2025** 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 **Mon Apr 28 14:55:28 CEST 2025** 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). @@ -5862,7 +5862,7 @@ This report was generated on **Mon Apr 28 12:49:00 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:00 CEST 2025** 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 **Mon Apr 28 14:55:28 CEST 2025** 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). @@ -6679,7 +6679,7 @@ This report was generated on **Mon Apr 28 12:49:00 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:01 CEST 2025** 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 **Mon Apr 28 14:55:28 CEST 2025** 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). @@ -7617,7 +7617,7 @@ This report was generated on **Mon Apr 28 12:49:01 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:01 CEST 2025** 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 **Mon Apr 28 14:55:29 CEST 2025** 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). @@ -8450,7 +8450,7 @@ This report was generated on **Mon Apr 28 12:49:01 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:01 CEST 2025** 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 **Mon Apr 28 14:55:29 CEST 2025** 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). @@ -9287,7 +9287,7 @@ This report was generated on **Mon Apr 28 12:49:01 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:01 CEST 2025** 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 **Mon Apr 28 14:55:29 CEST 2025** 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). @@ -10056,7 +10056,7 @@ This report was generated on **Mon Apr 28 12:49:01 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:01 CEST 2025** 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 **Mon Apr 28 14:55:29 CEST 2025** 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). @@ -10994,7 +10994,7 @@ This report was generated on **Mon Apr 28 12:49:01 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:02 CEST 2025** 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 **Mon Apr 28 14:55:29 CEST 2025** 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). @@ -11932,4 +11932,4 @@ This report was generated on **Mon Apr 28 12:49:02 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 12:49:02 CEST 2025** 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 **Mon Apr 28 14:55:30 CEST 2025** 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/model/src/main/kotlin/io/spine/validation/DistinctOption.kt b/model/src/main/kotlin/io/spine/validation/DistinctOption.kt index 8fee27c356..ef940251d1 100644 --- a/model/src/main/kotlin/io/spine/validation/DistinctOption.kt +++ b/model/src/main/kotlin/io/spine/validation/DistinctOption.kt @@ -129,7 +129,7 @@ internal class IfHasDuplicatesPolicy : Policy() { val option = event.option.unpack() val message = option.errorMsg - checkPlaceholders(message, field, file) + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, IF_HAS_DUPLICATES) return ifHasDuplicatesOptionDiscovered { id = field.ref @@ -170,15 +170,5 @@ private fun checkFieldType(field: Field, file: File) = */ private fun FieldType.isSupported(): Boolean = isMap || isList -private fun checkPlaceholders(template: String, field: Field, file: File) { - val missing = missingPlaceholders(template, SUPPORTED_PLACEHOLDERS) - Compilation.check(missing.isEmpty(), file, field.span) { - "The `${field.qualifiedName}` field specifies an error message using" + - " the `($IF_HAS_DUPLICATES)` option with unsupported placeholders: `$missing`." + - " Supported placeholders are the following:" + - " `${SUPPORTED_PLACEHOLDERS.map { it.value }}`." - } -} - private val SUPPORTED_PLACEHOLDERS = setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, FIELD_DUPLICATES) diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt index c48fd6c40b..40ffa89404 100644 --- a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt @@ -76,25 +76,3 @@ public enum class ErrorPlaceholder(public val value: String) { override fun toString(): String = value } - -/** - * Returns a set of placeholders that are used by the given [template] string, - * but not present in the provided [placeholders] set. - * - * @param template The template with placeholders like `${something}`. - * @param placeholders The set of error placeholders. - */ -public fun missingPlaceholders( - template: String, - placeholders: Set -): Set { - val requested = extractPlaceholders(template) - val provided = placeholders.map { it.value } - val missing = mutableSetOf() - for (placeholder in requested) { - if (!provided.contains(placeholder)) { - missing.add(placeholder) - } - } - return missing -} diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt new file mode 100644 index 0000000000..12687b30d8 --- /dev/null +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt @@ -0,0 +1,80 @@ +/* + * 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.validation + +import io.spine.protodata.Compilation +import io.spine.protodata.ast.Field +import io.spine.protodata.ast.File +import io.spine.protodata.ast.qualifiedName +import io.spine.protodata.check +import io.spine.validate.extractPlaceholders + +/** + * Checks if this [String] contains placeholders that are not present + * in the given set of the [supportedPlaceholders], and reports a compilation error if so. + * + * @param supportedPlaceholders The set of placeholders that can occur in this [String]. + * @param field The field declared the template. + * @param file The file that contains the [field] declaration. + * @param option The name of the option with which the message template was specified. + */ +internal fun String.checkPlaceholders( + supportedPlaceholders: Set, + field: Field, + file: File, + option: String +) { + val template = this + val missing = missingPlaceholders(template, supportedPlaceholders) + Compilation.check(missing.isEmpty(), file, field.span) { + "The `${field.qualifiedName}` field specifies an error message for the `($option)`" + + " option using unsupported placeholders: `$missing`. Supported placeholders are" + + " the following: `${supportedPlaceholders.map { it.value }}`." + } +} + +/** + * Returns a set of placeholders that are used by the given [template] string, + * but not present in the provided [placeholders] set. + * + * @param template The template with placeholders like `${something}`. + * @param placeholders The set of error placeholders. + */ +private fun missingPlaceholders( + template: String, + placeholders: Set +): Set { + val requested = extractPlaceholders(template) + val provided = placeholders.map { it.value } + val missing = mutableSetOf() + for (placeholder in requested) { + if (!provided.contains(placeholder)) { + missing.add(placeholder) + } + } + return missing +} diff --git a/model/src/main/kotlin/io/spine/validation/GoesOption.kt b/model/src/main/kotlin/io/spine/validation/GoesOption.kt index 306aa54357..68b1d2760a 100644 --- a/model/src/main/kotlin/io/spine/validation/GoesOption.kt +++ b/model/src/main/kotlin/io/spine/validation/GoesOption.kt @@ -96,7 +96,7 @@ internal class GoesPolicy : Policy() { checkCompanionType(companionField, file) val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } - checkPlaceholders(message, field, file) + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, GOES) return goesFieldDiscovered { id = field.ref @@ -146,18 +146,6 @@ private fun checkFieldsDistinct(field: Field, companion: Field, file: File) = " The invalid field: `${field.qualifiedName}`." } -private fun checkPlaceholders(template: String, field: Field, file: File) { - val missing = missingPlaceholders(template, SUPPORTED_PLACEHOLDERS) - Compilation.check(missing.isEmpty(), file, field.span) { - "The `${field.qualifiedName}` field specifies a custom error message for the `($GOES)`" + - " option using unsupported placeholders: `$missing`. Supported placeholders are" + - " the following: `${SUPPORTED_PLACEHOLDERS.map { it.value }}`." - } -} - -private val SUPPORTED_PLACEHOLDERS = - setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, GOES_COMPANION) - /** * Tells if this [FieldType] can be validated with the `(goes)` option. */ @@ -167,3 +155,6 @@ private fun FieldType.isSupported(): Boolean = private val SUPPORTED_PRIMITIVES = listOf( TYPE_STRING, TYPE_BYTES ) + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, GOES_COMPANION) diff --git a/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt b/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt index 67136f7ac2..6fe4290b9d 100644 --- a/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt @@ -129,7 +129,7 @@ internal class IfSetAgainPolicy : Policy() { val option = event.option.unpack() val message = option.errorMsg - checkPlaceholders(message, field, file) + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, IF_SET_AGAIN) return ifSetAgainOptionDiscovered { id = field.ref @@ -171,15 +171,5 @@ private fun checkFieldType(field: Field, file: File) = */ private fun FieldType.isSupported(): Boolean = !isList && !isMap -private fun checkPlaceholders(template: String, field: Field, file: File) { - val missing = missingPlaceholders(template, SUPPORTED_PLACEHOLDERS) - Compilation.check(missing.isEmpty(), file, field.span) { - "The `${field.qualifiedName}` field specifies an error message using" + - " the `($IF_SET_AGAIN)` option with unsupported placeholders: `$missing`." + - " Supported placeholders are the following:" + - " `${SUPPORTED_PLACEHOLDERS.map { it.value }}`." - } -} - private val SUPPORTED_PLACEHOLDERS = setOf(FIELD_VALUE, FIELD_PROPOSED_VALUE, FIELD_PATH, FIELD_TYPE, PARENT_TYPE) diff --git a/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt b/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt index 0ab4a6789b..8671c60b01 100644 --- a/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt +++ b/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt @@ -55,12 +55,12 @@ import io.spine.validation.IF_MISSING import io.spine.validation.OPTION_NAME import io.spine.validation.REQUIRED import io.spine.validation.checkBothApplied +import io.spine.validation.checkPlaceholders import io.spine.validation.defaultErrorMessage import io.spine.validation.event.IfMissingOptionDiscovered import io.spine.validation.event.RequiredFieldDiscovered import io.spine.validation.event.ifMissingOptionDiscovered import io.spine.validation.event.requiredFieldDiscovered -import io.spine.validation.missingPlaceholders import io.spine.validation.required.RequiredFieldSupport.isSupported /** @@ -134,7 +134,7 @@ internal class IfMissingPolicy : Policy() { val option = event.option.unpack() val message = option.errorMsg - checkPlaceholders(message, field, file) + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, IF_MISSING) return ifMissingOptionDiscovered { id = field.ref @@ -150,13 +150,4 @@ private fun checkFieldType(field: Field, file: File) = " strings, bytes, repeated, and maps." } -private fun checkPlaceholders(template: String, field: Field, file: File) { - val missing = missingPlaceholders(template, SUPPORTED_PLACEHOLDERS) - Compilation.check(missing.isEmpty(), file, field.span) { - "The `${field.qualifiedName}` field specifies an error message using the `($IF_MISSING)`" + - " option with unsupported placeholders: `$missing`. Supported placeholders are" + - " the following: `${SUPPORTED_PLACEHOLDERS.map { it.value }}`." - } -} - private val SUPPORTED_PLACEHOLDERS = setOf(FIELD_PATH, FIELD_TYPE, PARENT_TYPE) diff --git a/model/src/test/kotlin/io/spine/validation/DistinctOptionSpec.kt b/model/src/test/kotlin/io/spine/validation/DistinctOptionSpec.kt index 63eb67ac8a..0aa4f71517 100644 --- a/model/src/test/kotlin/io/spine/validation/DistinctOptionSpec.kt +++ b/model/src/test/kotlin/io/spine/validation/DistinctOptionSpec.kt @@ -56,7 +56,7 @@ internal class IfHasDuplicatesPolicySpec : CompilationErrorTest() { error.message.run { shouldContain(field.qualifiedName) shouldContain(IF_HAS_DUPLICATES) - shouldContain("with unsupported placeholders") + shouldContain("unsupported placeholders") shouldInclude("[field.name, duplicates.size]") } } diff --git a/model/src/test/kotlin/io/spine/validation/RequiredOptionSpec.kt b/model/src/test/kotlin/io/spine/validation/RequiredOptionSpec.kt index 432f8deddf..73c2033dd4 100644 --- a/model/src/test/kotlin/io/spine/validation/RequiredOptionSpec.kt +++ b/model/src/test/kotlin/io/spine/validation/RequiredOptionSpec.kt @@ -94,7 +94,7 @@ internal class IfMissingPolicySpec : CompilationErrorTest() { error.message.run { shouldContain(field.qualifiedName) shouldContain(IF_MISSING) - shouldContain("with unsupported placeholders") + shouldContain("unsupported placeholders") shouldInclude("[field.name, field.value]") } } diff --git a/model/src/test/kotlin/io/spine/validation/SetOnceOptionSpec.kt b/model/src/test/kotlin/io/spine/validation/SetOnceOptionSpec.kt index ae0fd84eeb..c288ee0358 100644 --- a/model/src/test/kotlin/io/spine/validation/SetOnceOptionSpec.kt +++ b/model/src/test/kotlin/io/spine/validation/SetOnceOptionSpec.kt @@ -79,7 +79,7 @@ internal class IfSetAgainPolicySpec : CompilationErrorTest() { error.message.run { shouldContain(field.qualifiedName) shouldContain(IF_SET_AGAIN) - shouldContain("with unsupported placeholders") + shouldContain("unsupported placeholders") shouldInclude("[field.name]") } } From 5d7a770108d11d727474b65f9f64afe9ee5f4080 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 15:16:03 +0200 Subject: [PATCH 04/19] Check `(pattern)` for unsupported placeholders --- .../io/spine/validation/ErrorPlaceholder.kt | 2 - .../io/spine/validation/PatternOption.kt | 19 +++- .../io/spine/validation/PatternPolicySpec.kt | 70 +++++++++++++++ .../io/spine/validation/GoesPolicyTestEnv.kt | 4 +- .../spine/validation/PatternPolicyTestEnv.kt | 52 +++++++++++ .../validation/pattern_option_spec.proto | 87 +++++++++++++++++++ 6 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt create mode 100644 model/src/testFixtures/kotlin/io/spine/validation/PatternPolicyTestEnv.kt create mode 100644 model/src/testFixtures/proto/spine/validation/pattern_option_spec.proto diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt index 40ffa89404..12498b36f8 100644 --- a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt @@ -26,8 +26,6 @@ package io.spine.validation -import io.spine.validate.extractPlaceholders - /** * A template placeholder that can be used in error messages. * diff --git a/model/src/main/kotlin/io/spine/validation/PatternOption.kt b/model/src/main/kotlin/io/spine/validation/PatternOption.kt index ae3004e43a..b3e01a7bdc 100644 --- a/model/src/main/kotlin/io/spine/validation/PatternOption.kt +++ b/model/src/main/kotlin/io/spine/validation/PatternOption.kt @@ -48,6 +48,12 @@ import io.spine.server.entity.alter import io.spine.server.event.Just import io.spine.server.event.React import io.spine.server.event.just +import io.spine.validation.ErrorPlaceholder.FIELD_PATH +import io.spine.validation.ErrorPlaceholder.FIELD_TYPE +import io.spine.validation.ErrorPlaceholder.FIELD_VALUE +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE +import io.spine.validation.ErrorPlaceholder.REGEX_MODIFIERS +import io.spine.validation.ErrorPlaceholder.REGEX_PATTERN import io.spine.validation.event.PatternFieldDiscovered import io.spine.validation.event.patternFieldDiscovered @@ -55,8 +61,12 @@ import io.spine.validation.event.patternFieldDiscovered * Controls whether a field should be validated with the `(pattern)` option. * * Whenever a field marked with the `(pattern)` option is discovered, emits - * [PatternFieldDiscovered] event. The policy reports a compilation error - * if the option does not support the field type. + * [PatternFieldDiscovered] event if the following conditions are met: + * + * 1. The field type is supported by the option. + * 2. The error message does not contain unsupported placeholders. + * + * Any violation of the above conditions leads to a compilation error. */ internal class PatternPolicy : Policy() { @@ -71,6 +81,8 @@ internal class PatternPolicy : Policy() { val option = event.option.unpack() val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, PATTERN) + return patternFieldDiscovered { id = field.ref errorMessage = message @@ -123,3 +135,6 @@ public val FieldType.isRepeatedString: Boolean */ public val FieldType.isSingularString: Boolean get() = primitive == TYPE_STRING + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, REGEX_PATTERN, REGEX_MODIFIERS) diff --git a/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt new file mode 100644 index 0000000000..ebdbd7f080 --- /dev/null +++ b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt @@ -0,0 +1,70 @@ +/* + * 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.validation + +import com.google.protobuf.Message +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldInclude +import io.spine.protodata.ast.name +import io.spine.protodata.ast.qualifiedName +import io.spine.protodata.protobuf.descriptor +import io.spine.protodata.protobuf.field +import kotlin.reflect.KClass +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +@DisplayName("`PatternPolicy` should reject the option when") +internal class PatternPolicySpec : CompilationErrorTest() { + + @MethodSource("io.spine.validation.PatternPolicyTestEnv#messagesWithUnsupportedTarget") + @ParameterizedTest(name = "when target field type is `{0}`") + fun targetFieldHasUnsupportedType(message: KClass) { + val descriptor = message.descriptor + val error = assertCompilationFails(descriptor) + val field = descriptor.field("value") + error.message.run { + shouldContain(field.type.name) + shouldContain(field.qualifiedName) + shouldContain("is not supported") + } + } + + @Test + fun `when the error message contains unsupported placeholders`() { + val message = PatternWithInvalidPlaceholders.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.qualifiedName) + shouldContain(PATTERN) + shouldContain("unsupported placeholders") + shouldInclude("[field.name, pattern.value]") + } + } +} diff --git a/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt b/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt index 59664a0481..9d23d25e79 100644 --- a/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt +++ b/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt @@ -36,7 +36,7 @@ import org.junit.jupiter.params.provider.Arguments.arguments object GoesPolicyTestEnv { /** - * Test data for [io.spine.validation.GoesPolicySpec.whenTargetFieldHasUnsupportedType]. + * Test data for [io.spine.validation.GoesPolicySpec.targetFieldHasUnsupportedType]. */ @JvmStatic fun messagesWithUnsupportedTarget() = listOf( @@ -56,7 +56,7 @@ object GoesPolicyTestEnv { ).map { arguments(named(it.first, it.second)) } /** - * Test data for [io.spine.validation.GoesPolicySpec.whenCompanionFieldHasUnsupportedType]. + * Test data for [io.spine.validation.GoesPolicySpec.companionFieldHasUnsupportedType]. */ @JvmStatic fun messagesWithUnsupportedCompanion() = listOf( diff --git a/model/src/testFixtures/kotlin/io/spine/validation/PatternPolicyTestEnv.kt b/model/src/testFixtures/kotlin/io/spine/validation/PatternPolicyTestEnv.kt new file mode 100644 index 0000000000..fea108946a --- /dev/null +++ b/model/src/testFixtures/kotlin/io/spine/validation/PatternPolicyTestEnv.kt @@ -0,0 +1,52 @@ +/* + * 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.validation + +import org.junit.jupiter.api.Named.named +import org.junit.jupiter.params.provider.Arguments.arguments + +/** + * Provides data for parametrized tests in [io.spine.validation.PatternPolicySpec]. + */ +@Suppress("unused") // Data provider for parameterized test. +object PatternPolicyTestEnv { + + /** + * Test data for [io.spine.validation.PatternPolicySpec.targetFieldHasUnsupportedType]. + */ + @JvmStatic + fun messagesWithUnsupportedTarget() = listOf( + "bool" to PatternBoolField::class, + "repeated double" to PatternRepeatedBoolField::class, + "int32" to PatternIntField::class, + "repeated int32" to PatternRepeatedIntField::class, + "double" to PatternDoubleField::class, + "repeated double" to PatternRepeatedDoubleField::class, + "message" to PatternMessageField::class, + "repeated message" to PatternRepeatedMessageField::class, + ).map { arguments(named(it.first, it.second)) } +} diff --git a/model/src/testFixtures/proto/spine/validation/pattern_option_spec.proto b/model/src/testFixtures/proto/spine/validation/pattern_option_spec.proto new file mode 100644 index 0000000000..43d7fea60d --- /dev/null +++ b/model/src/testFixtures/proto/spine/validation/pattern_option_spec.proto @@ -0,0 +1,87 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package spine.validation; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.validation"; +option java_outer_classname = "PatternOptionSpecProto"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; + +// Provides a boolean field with the inapplicable `(pattern)` option. +message PatternBoolField { + bool value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides a repeated boolean field with the inapplicable `(pattern)` option. +message PatternRepeatedBoolField { + repeated bool value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides an `int32` field with the inapplicable `(pattern)` option. +message PatternIntField { + int32 value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides an `int32` field with the inapplicable `(pattern)` option. +message PatternRepeatedIntField { + repeated int32 value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides a `double` field with the inapplicable `(pattern)` option. +message PatternDoubleField { + double value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides a repeated `double` field with the inapplicable `(pattern)` option. +message PatternRepeatedDoubleField { + double value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides a message field with the inapplicable `(pattern)` option. +message PatternMessageField { + google.protobuf.Timestamp value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides a message field with the inapplicable `(pattern)` option. +message PatternRepeatedMessageField { + repeated google.protobuf.Timestamp value = 1 [(pattern).regex = "^.{0,4}$"]; +} + +// Provides a `(pattern)` field that specifies a custom error message using +// the placeholders not supported by the option. +message PatternWithInvalidPlaceholders { + string value = 1 [ + (pattern).regex = "^.{0,4}$", + (pattern).error_msg = "The `${field.name}` does not match `${pattern.value}` regex." + ]; +} From b3736d842cea98c1e89fb992ee5b6695a119cd82 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 15:21:05 +0200 Subject: [PATCH 05/19] Check `(range)` for unsupported placeholders --- .../io/spine/validation/bound/RangeOption.kt | 11 +++++ .../io/spine/validation/RangePolicySpec.kt | 14 ++++++ .../spine/validation/goes_option_spec.proto | 2 +- .../spine/validation/range_option_spec.proto | 47 +++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 model/src/testFixtures/proto/spine/validation/range_option_spec.proto diff --git a/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt b/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt index c8af09592f..bc15d3902e 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt @@ -43,12 +43,18 @@ import io.spine.server.entity.alter import io.spine.server.event.Just import io.spine.server.event.React import io.spine.server.event.just +import io.spine.validation.ErrorPlaceholder.FIELD_PATH +import io.spine.validation.ErrorPlaceholder.FIELD_TYPE +import io.spine.validation.ErrorPlaceholder.FIELD_VALUE +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE +import io.spine.validation.ErrorPlaceholder.RANGE_VALUE import io.spine.validation.OPTION_NAME import io.spine.validation.RANGE import io.spine.validation.bound.BoundFieldSupport.checkFieldType import io.spine.validation.defaultMessage import io.spine.validation.bound.event.RangeFieldDiscovered import io.spine.validation.bound.event.rangeFieldDiscovered +import io.spine.validation.checkPlaceholders /** * Controls whether a field should be validated with the `(range)` option. @@ -99,6 +105,8 @@ internal class RangePolicy : Policy() { context.checkRelation(lower, upper) val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, RANGE) + return rangeFieldDiscovered { id = field.ref subject = field @@ -169,3 +177,6 @@ private fun RangeContext.checkRelation(lower: KotlinNumericBound, upper: KotlinN } private val DELIMITER = Regex("""(?<=\d)\s?\.\.\s?(?=[\d-+])""") + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, RANGE_VALUE) diff --git a/model/src/test/kotlin/io/spine/validation/RangePolicySpec.kt b/model/src/test/kotlin/io/spine/validation/RangePolicySpec.kt index 39cd8a5d28..5035ad5b1f 100644 --- a/model/src/test/kotlin/io/spine/validation/RangePolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/RangePolicySpec.kt @@ -28,6 +28,7 @@ package io.spine.validation import com.google.protobuf.Message import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldInclude import io.spine.protodata.ast.Field import io.spine.protodata.ast.name import io.spine.protodata.ast.qualifiedName @@ -138,6 +139,19 @@ internal class RangePolicySpec : CompilationErrorTest() { shouldContain("could not parse the `15.0` bound value") shouldContain("make sure the provided value is an integer number") } + + @Test + fun `with unsupported placeholders in the error message`() { + val message = RangeWithInvalidPlaceholders.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.qualifiedName) + shouldContain(RANGE) + shouldContain("unsupported placeholders") + shouldInclude("[field.name, range]") + } + } } private fun CompilationErrorTest.assertCompilationFails( diff --git a/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto b/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto index abf780ecb3..c37daf01ea 100644 --- a/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto +++ b/model/src/testFixtures/proto/spine/validation/goes_option_spec.proto @@ -42,7 +42,7 @@ import "google/protobuf/timestamp.proto"; message GoesWithInvalidPlaceholders { string companion = 1; string value = 2 [ - (.goes).with = "companion", + (goes).with = "companion", (goes).error_msg = "The `${field.name}` field does not have a value." ]; } diff --git a/model/src/testFixtures/proto/spine/validation/range_option_spec.proto b/model/src/testFixtures/proto/spine/validation/range_option_spec.proto new file mode 100644 index 0000000000..45812e46f9 --- /dev/null +++ b/model/src/testFixtures/proto/spine/validation/range_option_spec.proto @@ -0,0 +1,47 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package spine.validation; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.validation"; +option java_outer_classname = "RangeOptionSpecProto"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; + +// Provides a `(range)` field that specifies a custom error message using +// the placeholders not supported by the option. +message RangeWithInvalidPlaceholders { + int32 value = 2 [ + (range).value = "[1 .. 10]", + (range).error_msg = "The `${field.name}` does not belong to the `${range}` range." + ]; +} From e4a7b22208080302460e07bc067a656133fcf161 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 15:25:18 +0200 Subject: [PATCH 06/19] Check `(min)` and `(max)` for unsupported placeholders --- .../kotlin/io/spine/validation/bound/MaxOption.kt | 15 +++++++++++++++ .../kotlin/io/spine/validation/bound/MinOption.kt | 15 +++++++++++++++ .../io/spine/validation/bound/RangeOption.kt | 1 + 3 files changed, 31 insertions(+) diff --git a/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt b/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt index 022700f19d..1716499df4 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt @@ -40,16 +40,26 @@ import io.spine.server.entity.alter import io.spine.server.event.Just import io.spine.server.event.React import io.spine.server.event.just +import io.spine.validation.ErrorPlaceholder.FIELD_PATH +import io.spine.validation.ErrorPlaceholder.FIELD_TYPE +import io.spine.validation.ErrorPlaceholder.FIELD_VALUE +import io.spine.validation.ErrorPlaceholder.MAX_OPERATOR +import io.spine.validation.ErrorPlaceholder.MAX_VALUE +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE import io.spine.validation.MAX import io.spine.validation.OPTION_NAME +import io.spine.validation.RANGE import io.spine.validation.bound.BoundFieldSupport.checkFieldType import io.spine.validation.defaultMessage import io.spine.validation.bound.event.MaxFieldDiscovered import io.spine.validation.bound.event.maxFieldDiscovered +import io.spine.validation.checkPlaceholders /** * A policy to add a validation rule to a type whenever the `(max)` field option * is discovered. + * + * The condition checks done by the policy are similar to the ones performed by [RangePolicy]. */ internal class MaxPolicy : Policy() { @@ -67,6 +77,8 @@ internal class MaxPolicy : Policy() { val kotlinBound = context.checkNumericBound(option.value, option.exclusive) val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, RANGE) + return maxFieldDiscovered { id = field.ref subject = field @@ -92,3 +104,6 @@ internal class MaxFieldView : View() { file = e.file } } + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, MAX_VALUE, MAX_OPERATOR) diff --git a/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt b/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt index ddd0404bbd..d4cac12034 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt @@ -40,16 +40,26 @@ import io.spine.server.entity.alter import io.spine.server.event.Just import io.spine.server.event.React import io.spine.server.event.just +import io.spine.validation.ErrorPlaceholder.FIELD_PATH +import io.spine.validation.ErrorPlaceholder.FIELD_TYPE +import io.spine.validation.ErrorPlaceholder.FIELD_VALUE +import io.spine.validation.ErrorPlaceholder.MIN_OPERATOR +import io.spine.validation.ErrorPlaceholder.MIN_VALUE +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE import io.spine.validation.MIN import io.spine.validation.OPTION_NAME +import io.spine.validation.RANGE import io.spine.validation.bound.BoundFieldSupport.checkFieldType import io.spine.validation.defaultMessage import io.spine.validation.bound.event.MinFieldDiscovered import io.spine.validation.bound.event.minFieldDiscovered +import io.spine.validation.checkPlaceholders /** * A policy to add a validation rule to a type whenever the `(min)` field option * is discovered. + * + * The condition checks done by the policy are similar to the ones performed by [RangePolicy]. */ internal class MinPolicy : Policy() { @@ -67,6 +77,8 @@ internal class MinPolicy : Policy() { val kotlinBound = context.checkNumericBound(option.value, option.exclusive) val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, RANGE) + return minFieldDiscovered { id = field.ref subject = field @@ -92,3 +104,6 @@ internal class MinFieldView : View() { file = e.file } } + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, MIN_VALUE, MIN_OPERATOR) diff --git a/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt b/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt index bc15d3902e..129b9a5587 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt @@ -69,6 +69,7 @@ import io.spine.validation.checkPlaceholders * for integer fields. * 5. The provided bounds fit into the range of the target field type. * 6. The lower bound is strictly less than the upper one. + * 7. The error message does not contain unsupported placeholders. * * Any violation of the above conditions leads to a compilation error. * From 0e6e252c7261e0c8439b0cbec98e0fa7ea418487 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 17:04:12 +0200 Subject: [PATCH 07/19] Check `(choice)` for unsupported placeholders --- .../io/spine/validation/ChoiceOption.kt | 20 +++++-- .../io/spine/validation/ErrorPlaceholders.kt | 27 ++++++++- .../io/spine/validation/ChoicePolicySpec.kt | 49 ++++++++++++++++ .../io/spine/validation/PatternPolicySpec.kt | 2 +- .../spine/validation/choice_option_spec.proto | 56 +++++++++++++++++++ 5 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 model/src/test/kotlin/io/spine/validation/ChoicePolicySpec.kt create mode 100644 model/src/testFixtures/proto/spine/validation/choice_option_spec.proto diff --git a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index 97351e5684..7ed8b1861c 100644 --- a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt @@ -43,6 +43,8 @@ import io.spine.server.event.NoReaction import io.spine.server.event.React import io.spine.server.event.asA import io.spine.server.tuple.EitherOf2 +import io.spine.validation.ErrorPlaceholder.GROUP_PATH +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE import io.spine.validation.event.ChoiceOneofDiscovered import io.spine.validation.event.choiceOneofDiscovered @@ -50,8 +52,13 @@ import io.spine.validation.event.choiceOneofDiscovered * Controls whether a `oneof` group should be validated with the `(choice)` option. * * Whenever a `oneof` groupd marked with `(choice)` option is discovered, - * emits [ChoiceOneofDiscovered] event if the option has the `required` flag - * set to `true`. Otherwise, the policy emits [NoReaction]. + * emits [ChoiceOneofDiscovered] event if the following conditions are met: + * + * 1. The option has the `required` flag set to `true`. + * 2. The error message does not contain unsupported placeholders. + * + * If (1) is violated, the policy just emits [NoReaction]. + * Violation of (2) leads to a compilation error. * * Note that unlike the `(required)` constraint, this option supports any field type. * Protobuf encodes a non-set value as a special case, allowing for checking whether @@ -64,13 +71,16 @@ internal class ChoicePolicy : Policy() { @External @Where(field = OPTION_NAME, equals = CHOICE) event: OneofOptionDiscovered ): EitherOf2 { + val oneof = event.subject + val file = event.file val option = event.option.unpack() + val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, oneof, file, CHOICE) + if (!option.required) { return ignore() } - val oneof = event.subject - val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } return choiceOneofDiscovered { id = oneof.ref subject = oneof @@ -108,3 +118,5 @@ internal class ChoiceGroupView : View, + oneof: OneofGroup, + file: File, + option: String +) { + val template = this + val missing = missingPlaceholders(template, supportedPlaceholders) + Compilation.check(missing.isEmpty(), file, oneof.span) { + "The `${oneof.qualifiedName}` group specifies an error message for the `($option)`" + + " option using unsupported placeholders: `$missing`. Supported placeholders are" + + " the following: `${supportedPlaceholders.map { it.value }}`." + } +} + +/** + * Checks if this [String] contains placeholders that are not present + * in the given set of the [supportedPlaceholders], and reports a compilation error if so. + * + * @param supportedPlaceholders The set of placeholders that can occur in this [String]. + * @param field The field declared the message template. * @param file The file that contains the [field] declaration. * @param option The name of the option with which the message template was specified. */ diff --git a/model/src/test/kotlin/io/spine/validation/ChoicePolicySpec.kt b/model/src/test/kotlin/io/spine/validation/ChoicePolicySpec.kt new file mode 100644 index 0000000000..b609156efd --- /dev/null +++ b/model/src/test/kotlin/io/spine/validation/ChoicePolicySpec.kt @@ -0,0 +1,49 @@ +/* + * 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.validation + +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldInclude +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`ChoicePolicy` should reject the option") +internal class ChoicePolicySpec : CompilationErrorTest() { + + @Test + fun `when the error message contains unsupported placeholders`() { + val message = ChoiceWithInvalidPlaceholders.getDescriptor() + val error = assertCompilationFails(message) + val oneof = message.oneofs.first { it.name == "value" } + error.message.run { + shouldContain(oneof.fullName) + shouldContain(CHOICE) + shouldContain("unsupported placeholders") + shouldInclude("[group.fields]") + } + } +} diff --git a/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt index ebdbd7f080..57f9b005e8 100644 --- a/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt @@ -56,7 +56,7 @@ internal class PatternPolicySpec : CompilationErrorTest() { } @Test - fun `when the error message contains unsupported placeholders`() { + fun `the error message contains unsupported placeholders`() { val message = PatternWithInvalidPlaceholders.getDescriptor() val error = assertCompilationFails(message) val field = message.field("value") diff --git a/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto b/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto new file mode 100644 index 0000000000..88174b8a07 --- /dev/null +++ b/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto @@ -0,0 +1,56 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package spine.validation; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.validation"; +option java_outer_classname = "ChoiceOptionSpecProto"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; + +// Provides a `(choice)` oneof group that specifies a custom error message using +// the placeholders not supported by the option. +message ChoiceWithInvalidPlaceholders { + + oneof value { + + option (choice) = { + required: true; + error_msg: "`${group.path}` should have value set for any of `${group.fields}`." + }; + + string string_value = 2; + + bool bool_value = 3; + + } +} From f64eae921b986f7ca9947ba81990c771f50cb0de Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 17:18:28 +0200 Subject: [PATCH 08/19] Check `(require)` for unsupported placeholders --- .../io/spine/validation/ErrorPlaceholders.kt | 25 +++++++++++++++++++ .../validation/required/RequireOption.kt | 19 ++++++++++++-- .../io/spine/validation/RequirePolicySpec.kt | 13 ++++++++++ .../validation/require_option_spec.proto | 10 ++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt index 23200e25ed..d240dd0c26 100644 --- a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt @@ -29,11 +29,36 @@ package io.spine.validation import io.spine.protodata.Compilation import io.spine.protodata.ast.Field import io.spine.protodata.ast.File +import io.spine.protodata.ast.MessageType import io.spine.protodata.ast.OneofGroup import io.spine.protodata.ast.qualifiedName import io.spine.protodata.check import io.spine.validate.extractPlaceholders +/** + * Checks if this [String] contains placeholders that are not present + * in the given set of the [supportedPlaceholders], and reports a compilation error if so. + * + * @param supportedPlaceholders The set of placeholders that can occur in this [String]. + * @param message The message type declared the message template. + * @param file The file that contains the [message] declaration. + * @param option The name of the option with which the message template was specified. + */ +internal fun String.checkPlaceholders( + supportedPlaceholders: Set, + message: MessageType, + file: File, + option: String +) { + val template = this + val missing = missingPlaceholders(template, supportedPlaceholders) + Compilation.check(missing.isEmpty(), file, message.span) { + "The `${message.qualifiedName}` message specifies an error message for the `($option)`" + + " option using unsupported placeholders: `$missing`. Supported placeholders are" + + " the following: `${supportedPlaceholders.map { it.value }}`." + } +} + /** * Checks if this [String] contains placeholders that are not present * in the given set of the [supportedPlaceholders], and reports a compilation error if so. diff --git a/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt b/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt index eb225feaff..9c26dfc309 100644 --- a/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt +++ b/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt @@ -39,9 +39,12 @@ import io.spine.server.entity.alter import io.spine.server.event.Just import io.spine.server.event.React import io.spine.server.event.just +import io.spine.validation.ErrorPlaceholder.MESSAGE_TYPE +import io.spine.validation.ErrorPlaceholder.REQUIRE_FIELDS import io.spine.validation.OPTION_NAME import io.spine.validation.REQUIRE import io.spine.validation.RequireMessage +import io.spine.validation.checkPlaceholders import io.spine.validation.defaultMessage import io.spine.validation.event.RequireMessageDiscovered import io.spine.validation.event.requireMessageDiscovered @@ -50,8 +53,13 @@ import io.spine.validation.event.requireMessageDiscovered * Controls whether a message should be validated with the `(require)` option. * * Whenever a message marked with `(require)` option is discovered, emits - * [RequireMessageDiscovered] event if the specified field groups are valid. - * Please take a look on docs to [ParseFieldGroups] to see how they are validated. + * [RequireMessageDiscovered] event if the following conditions are met: + * + * 1. The specified field groups are valid. Please take a look on docs to [ParseFieldGroups] + * to see how they are validated. + * 2. The error message does not contain unsupported placeholders. + * + * Any violation of the above conditions leads to a compilation error. */ internal class RequirePolicy : Policy() { @@ -61,9 +69,14 @@ internal class RequirePolicy : Policy() { event: MessageOptionDiscovered, ): Just { val messageType = event.subject + val file = event.file + val option = event.option.unpack() val groups = ParseFieldGroups(option, messageType, event.file).result + val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, messageType, file, REQUIRE) + return requireMessageDiscovered { id = messageType.name errorMessage = message @@ -86,3 +99,5 @@ internal class RequireMessageView : View Date: Mon, 28 Apr 2025 17:28:10 +0200 Subject: [PATCH 09/19] Check `(when)` for unsupported placeholders --- .../kotlin/io/spine/validation/WhenOption.kt | 10 +++ .../io/spine/validation/WhenPolicySpec.kt | 85 +++++++++++++++++++ .../spine/validation/when_option_spec.proto | 63 ++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt create mode 100644 model/src/testFixtures/proto/spine/validation/when_option_spec.proto diff --git a/model/src/main/kotlin/io/spine/validation/WhenOption.kt b/model/src/main/kotlin/io/spine/validation/WhenOption.kt index d4cfd53975..459efd7f03 100644 --- a/model/src/main/kotlin/io/spine/validation/WhenOption.kt +++ b/model/src/main/kotlin/io/spine/validation/WhenOption.kt @@ -58,6 +58,11 @@ import io.spine.time.validation.TimeOption import io.spine.validation.event.WhenFieldDiscovered import io.spine.validation.event.whenFieldDiscovered import io.spine.protodata.java.findJavaClassName +import io.spine.validation.ErrorPlaceholder.FIELD_PATH +import io.spine.validation.ErrorPlaceholder.FIELD_TYPE +import io.spine.validation.ErrorPlaceholder.FIELD_VALUE +import io.spine.validation.ErrorPlaceholder.PARENT_TYPE +import io.spine.validation.ErrorPlaceholder.WHEN_IN import io.spine.validation.TimeFieldType.TFT_TEMPORAL import io.spine.validation.TimeFieldType.TFT_TIMESTAMP import io.spine.validation.TimeFieldType.TFT_UNKNOWN @@ -94,6 +99,8 @@ internal class WhenPolicy : Policy() { } val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, WHEN) + return whenFieldDiscovered { id = field.ref subject = field @@ -156,3 +163,6 @@ internal class WhenFieldView : View() { type = e.type } } + +private val SUPPORTED_PLACEHOLDERS = + setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, WHEN_IN) diff --git a/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt new file mode 100644 index 0000000000..8f79f20c58 --- /dev/null +++ b/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt @@ -0,0 +1,85 @@ +/* + * 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.validation + +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldInclude +import io.spine.protodata.ast.name +import io.spine.protodata.ast.qualifiedName +import io.spine.protodata.protobuf.field +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("`WhenPolicy` should") +internal class WhenPolicySpec : CompilationErrorTest() { + + @Test + fun `reject option on a boolean field`() { + val message = WhenBoolField.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.type.name) + shouldContain(field.qualifiedName) + shouldContain("is not supported") + } } + + @Test + fun `reject option on an integer field`() { + val message = WhenInt32Field.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.type.name) + shouldContain(field.qualifiedName) + shouldContain("is not supported") + } } + + @Test + fun `reject option on a string field`() { + val message = WhenStringField.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.type.name) + shouldContain(field.qualifiedName) + shouldContain("is not supported") + } } + + @Test + fun `the error message contains unsupported placeholders`() { + val message = WhenWithInvalidPlaceholders.getDescriptor() + val error = assertCompilationFails(message) + val field = message.field("value") + error.message.run { + shouldContain(field.qualifiedName) + shouldContain(WHEN) + shouldContain("unsupported placeholders") + shouldInclude("[when]") + } + } +} diff --git a/model/src/testFixtures/proto/spine/validation/when_option_spec.proto b/model/src/testFixtures/proto/spine/validation/when_option_spec.proto new file mode 100644 index 0000000000..91e2004adf --- /dev/null +++ b/model/src/testFixtures/proto/spine/validation/when_option_spec.proto @@ -0,0 +1,63 @@ +/* + * 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. + */ + +syntax = "proto3"; + +package spine.validation; + +import "spine/options.proto"; +import "spine/time_options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.validation"; +option java_outer_classname = "WhenOptionSpecProto"; +option java_multiple_files = true; + +import "google/protobuf/timestamp.proto"; + +// Provides a boolean field with the inapplicable `(when)` option. +message WhenBoolField { + bool value = 1 [(when).in = FUTURE]; +} + +// Provides an int32 field with the inapplicable `(when)` option. +message WhenInt32Field { + int32 value = 1 [(when).in = FUTURE]; +} + +// Provides a string field with the inapplicable `(when)` option. +message WhenStringField { + string value = 1 [(when).in = PAST]; +} + +// Provides a `(when)` field that specifies a custom error message using +// the placeholders not supported by the option. +message WhenWithInvalidPlaceholders { + google.protobuf.Timestamp value = 1 [(when) = { + in: PAST, + error_msg: "The field value `${field.value}` must be in `${when}`." + }]; +} From 2c01b725166a4ca90fb8e2f5a047a2b55fa4824f Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 17:28:30 +0200 Subject: [PATCH 10/19] Bump the library version -> `2.0.0-SNAPSHOT.316` --- version.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle.kts b/version.gradle.kts index 8bb0d81d9e..bee4cdb422 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -29,4 +29,4 @@ * * For Spine-based dependencies please see [io.spine.dependency.local.Spine]. */ -val validationVersion by extra("2.0.0-SNAPSHOT.315") +val validationVersion by extra("2.0.0-SNAPSHOT.316") From aa242f5d53146c238bf4a5790dfe74f24443d2b4 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Mon, 28 Apr 2025 17:32:04 +0200 Subject: [PATCH 11/19] Update reports --- dependencies.md | 56 ++++++++++++++++++++++++------------------------- pom.xml | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/dependencies.md b/dependencies.md index 57a70922ca..dc3e114e94 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.validation:spine-validation-java:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-java:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -861,12 +861,12 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:27 CEST 2025** 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 **Mon Apr 28 17:30:05 CEST 2025** 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.validation:spine-validation-java-bundle:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-java-bundle:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -1466,12 +1466,12 @@ This report was generated on **Mon Apr 28 14:55:27 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:27 CEST 2025** 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 **Mon Apr 28 17:30:05 CEST 2025** 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.validation:spine-validation-java-runtime:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-java-runtime:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2154,12 +2154,12 @@ This report was generated on **Mon Apr 28 14:55:27 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:27 CEST 2025** 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 **Mon Apr 28 17:30:05 CEST 2025** 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.validation:spine-validation-java-tests:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-java-tests:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -3128,12 +3128,12 @@ This report was generated on **Mon Apr 28 14:55:27 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:28 CEST 2025** 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 **Mon Apr 28 17:30:05 CEST 2025** 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.validation:spine-validation-model:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-model:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -4018,12 +4018,12 @@ This report was generated on **Mon Apr 28 14:55:28 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:28 CEST 2025** 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 **Mon Apr 28 17:30:06 CEST 2025** 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.validation:spine-validation-proto:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-proto:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -4972,12 +4972,12 @@ This report was generated on **Mon Apr 28 14:55:28 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:28 CEST 2025** 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 **Mon Apr 28 17:30:06 CEST 2025** 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.validation:spine-validation-consumer:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-consumer:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -5862,12 +5862,12 @@ This report was generated on **Mon Apr 28 14:55:28 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:28 CEST 2025** 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 **Mon Apr 28 17:30:06 CEST 2025** 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.validation:spine-validation-consumer-dependency:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-consumer-dependency:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -6679,12 +6679,12 @@ This report was generated on **Mon Apr 28 14:55:28 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:28 CEST 2025** 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 **Mon Apr 28 17:30:06 CEST 2025** 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.validation:spine-validation-extensions:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-extensions:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -7617,12 +7617,12 @@ This report was generated on **Mon Apr 28 14:55:28 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:29 CEST 2025** 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 **Mon Apr 28 17:30:07 CEST 2025** 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.validation:spine-validation-runtime:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-runtime:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8450,12 +8450,12 @@ This report was generated on **Mon Apr 28 14:55:29 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:29 CEST 2025** 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 **Mon Apr 28 17:30:07 CEST 2025** 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.validation:spine-validation-validating:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-validating:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9287,12 +9287,12 @@ This report was generated on **Mon Apr 28 14:55:29 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:29 CEST 2025** 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 **Mon Apr 28 17:30:07 CEST 2025** 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.validation:spine-validation-vanilla:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-vanilla:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -10056,12 +10056,12 @@ This report was generated on **Mon Apr 28 14:55:29 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:29 CEST 2025** 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 **Mon Apr 28 17:30:07 CEST 2025** 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.validation:spine-validation-configuration:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-configuration:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -10994,12 +10994,12 @@ This report was generated on **Mon Apr 28 14:55:29 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:29 CEST 2025** 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 **Mon Apr 28 17:30:07 CEST 2025** 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.validation:spine-validation-context:2.0.0-SNAPSHOT.315` +# Dependencies of `io.spine.validation:spine-validation-context:2.0.0-SNAPSHOT.316` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.15.3. @@ -11932,4 +11932,4 @@ This report was generated on **Mon Apr 28 14:55:29 CEST 2025** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Mon Apr 28 14:55:30 CEST 2025** 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 **Mon Apr 28 17:30:07 CEST 2025** 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 085f326c63..d7b3c85dcb 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.validation validation -2.0.0-SNAPSHOT.315 +2.0.0-SNAPSHOT.316 2015 From a49fe4c879b64db196553637c6c9d23bdc077f80 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 12:12:48 +0200 Subject: [PATCH 12/19] Remove duplicate string literals --- .../io/spine/validation/ChoiceOption.kt | 4 +- .../io/spine/validation/ErrorPlaceholders.kt | 79 +++++++++++-------- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index 7ed8b1861c..5b778b7073 100644 --- a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt @@ -51,13 +51,13 @@ import io.spine.validation.event.choiceOneofDiscovered /** * Controls whether a `oneof` group should be validated with the `(choice)` option. * - * Whenever a `oneof` groupd marked with `(choice)` option is discovered, + * Whenever a `oneof` groupd marked with the `(choice)` option is discovered, * emits [ChoiceOneofDiscovered] event if the following conditions are met: * * 1. The option has the `required` flag set to `true`. * 2. The error message does not contain unsupported placeholders. * - * If (1) is violated, the policy just emits [NoReaction]. + * If (1) is violated, the policy emits [NoReaction]. * Violation of (2) leads to a compilation error. * * Note that unlike the `(required)` constraint, this option supports any field type. diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt index d240dd0c26..b07596a37d 100644 --- a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt @@ -31,79 +31,94 @@ import io.spine.protodata.ast.Field import io.spine.protodata.ast.File import io.spine.protodata.ast.MessageType import io.spine.protodata.ast.OneofGroup +import io.spine.protodata.ast.Span import io.spine.protodata.ast.qualifiedName import io.spine.protodata.check import io.spine.validate.extractPlaceholders /** * Checks if this [String] contains placeholders that are not present - * in the given set of the [supportedPlaceholders], and reports a compilation error if so. + * in the given set of the [supported], and reports a compilation error if so. * - * @param supportedPlaceholders The set of placeholders that can occur in this [String]. + * @param supported The set of placeholders that can occur in this [String]. * @param message The message type declared the message template. * @param file The file that contains the [message] declaration. * @param option The name of the option with which the message template was specified. */ internal fun String.checkPlaceholders( - supportedPlaceholders: Set, + supported: Set, message: MessageType, file: File, option: String -) { - val template = this - val missing = missingPlaceholders(template, supportedPlaceholders) - Compilation.check(missing.isEmpty(), file, message.span) { - "The `${message.qualifiedName}` message specifies an error message for the `($option)`" + - " option using unsupported placeholders: `$missing`. Supported placeholders are" + - " the following: `${supportedPlaceholders.map { it.value }}`." - } -} +) = checkPlaceholders( + supported, + "`${message.qualifiedName}` message", + message.span, + file, + option +) /** * Checks if this [String] contains placeholders that are not present - * in the given set of the [supportedPlaceholders], and reports a compilation error if so. + * in the given set of the [supported], and reports a compilation error if so. * - * @param supportedPlaceholders The set of placeholders that can occur in this [String]. + * @param supported The set of placeholders that can occur in this [String]. * @param oneof The oneof group declared the message template. * @param file The file that contains the [oneof] declaration. * @param option The name of the option with which the message template was specified. */ internal fun String.checkPlaceholders( - supportedPlaceholders: Set, + supported: Set, oneof: OneofGroup, file: File, option: String -) { - val template = this - val missing = missingPlaceholders(template, supportedPlaceholders) - Compilation.check(missing.isEmpty(), file, oneof.span) { - "The `${oneof.qualifiedName}` group specifies an error message for the `($option)`" + - " option using unsupported placeholders: `$missing`. Supported placeholders are" + - " the following: `${supportedPlaceholders.map { it.value }}`." - } -} +) = checkPlaceholders( + supported, + "`${oneof.qualifiedName}` group", + oneof.span, + file, + option +) /** * Checks if this [String] contains placeholders that are not present - * in the given set of the [supportedPlaceholders], and reports a compilation error if so. + * in the given set of the [supported], and reports a compilation error if so. * - * @param supportedPlaceholders The set of placeholders that can occur in this [String]. + * @param supported The set of placeholders that can occur in this [String]. * @param field The field declared the message template. * @param file The file that contains the [field] declaration. * @param option The name of the option with which the message template was specified. */ internal fun String.checkPlaceholders( - supportedPlaceholders: Set, + supported: Set, field: Field, file: File, option: String +) = checkPlaceholders( + supported, + "`${field.qualifiedName}` field", + field.span, + file, + option +) + +/** + * Checks if this [String] contains placeholders that are not present + * in the given set of the [supported], and reports a compilation error if so. + */ +private fun String.checkPlaceholders( + supported: Set, + declaration: String, + span: Span, + file: File, + option: String ) { val template = this - val missing = missingPlaceholders(template, supportedPlaceholders) - Compilation.check(missing.isEmpty(), file, field.span) { - "The `${field.qualifiedName}` field specifies an error message for the `($option)`" + - " option using unsupported placeholders: `$missing`. Supported placeholders are" + - " the following: `${supportedPlaceholders.map { it.value }}`." + val missing = missingPlaceholders(template, supported) + Compilation.check(missing.isEmpty(), file, span) { + "The $declaration specifies an error message for the `($option)` option using unsupported" + + " placeholders: `$missing`. Supported placeholders are the following:" + + " `${supported.map { it.value }}`." } } From c28b6e127f8b6a6c8947a5893be9910805d367e2 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 12:16:43 +0200 Subject: [PATCH 13/19] Proofread docs --- .../io/spine/validation/ErrorPlaceholders.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt index b07596a37d..c0d6ea4acc 100644 --- a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt @@ -37,11 +37,11 @@ import io.spine.protodata.check import io.spine.validate.extractPlaceholders /** - * Checks if this [String] contains placeholders that are not present - * in the given set of the [supported], and reports a compilation error if so. + * Checks if this [String] contains placeholders that are not present in the given + * set of the [supported] placeholders, and reports a compilation error if so. * * @param supported The set of placeholders that can occur in this [String]. - * @param message The message type declared the message template. + * @param message The message type declared that the message template. * @param file The file that contains the [message] declaration. * @param option The name of the option with which the message template was specified. */ @@ -59,11 +59,11 @@ internal fun String.checkPlaceholders( ) /** - * Checks if this [String] contains placeholders that are not present - * in the given set of the [supported], and reports a compilation error if so. + * Checks if this [String] contains placeholders that are not present in the given + * set of the [supported] placeholders, and reports a compilation error if so. * * @param supported The set of placeholders that can occur in this [String]. - * @param oneof The oneof group declared the message template. + * @param oneof The oneof group that declared the message template. * @param file The file that contains the [oneof] declaration. * @param option The name of the option with which the message template was specified. */ @@ -81,11 +81,11 @@ internal fun String.checkPlaceholders( ) /** - * Checks if this [String] contains placeholders that are not present - * in the given set of the [supported], and reports a compilation error if so. + * Checks if this [String] contains placeholders that are not present in the given + * set of the [supported] placeholders, and reports a compilation error if so. * * @param supported The set of placeholders that can occur in this [String]. - * @param field The field declared the message template. + * @param field The field that declared the message template. * @param file The file that contains the [field] declaration. * @param option The name of the option with which the message template was specified. */ @@ -103,8 +103,8 @@ internal fun String.checkPlaceholders( ) /** - * Checks if this [String] contains placeholders that are not present - * in the given set of the [supported], and reports a compilation error if so. + * Checks if this [String] contains placeholders that are not present in the given + * set of the [supported] placeholders, and reports a compilation error if so. */ private fun String.checkPlaceholders( supported: Set, From 69b64f77d3a935efcc26809074ce0d93f7ebd938 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 12:19:34 +0200 Subject: [PATCH 14/19] Actualize docs to `(when)` --- model/src/main/kotlin/io/spine/validation/WhenOption.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/model/src/main/kotlin/io/spine/validation/WhenOption.kt b/model/src/main/kotlin/io/spine/validation/WhenOption.kt index 459efd7f03..dc7c727c97 100644 --- a/model/src/main/kotlin/io/spine/validation/WhenOption.kt +++ b/model/src/main/kotlin/io/spine/validation/WhenOption.kt @@ -74,11 +74,12 @@ import io.spine.validation.TimeFieldType.TFT_UNKNOWN * [WhenFieldDiscovered] event if the following conditions are met: * * 1) The field type is supported by the option. - * 2) The option value is other than [Time.TIME_UNDEFINED]. + * 2) The error message does not contain unsupported placeholders. + * 3) The option value is other than [Time.TIME_UNDEFINED]. * - * If (1) is violated, the policy reports a compilation error. + * If (1) or (2) is violated, the policy reports a compilation error. * - * Violation of (2) means that the `(when)` option is applied correctly, + * Violation of (3) means that the `(when)` option is applied correctly, * but disabled. In this case, the policy emits [NoReaction]. */ internal class WhenPolicy : Policy() { From 215bddab7f0e35561067574e1f7d2ac608c84ffb Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 12:46:09 +0200 Subject: [PATCH 15/19] Rollback naming of test cases --- .../test/kotlin/io/spine/validation/GoesPolicySpec.kt | 10 +++++----- .../kotlin/io/spine/validation/PatternPolicySpec.kt | 6 +++--- .../test/kotlin/io/spine/validation/WhenPolicySpec.kt | 10 +++++----- .../kotlin/io/spine/validation/GoesPolicyTestEnv.kt | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt index f577ff96d0..7edb092950 100644 --- a/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt @@ -41,12 +41,12 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource -@DisplayName("`GoesPolicy` should reject the option when") +@DisplayName("`GoesPolicy` should reject the option") internal class GoesPolicySpec : CompilationErrorTest() { @MethodSource("io.spine.validation.GoesPolicyTestEnv#messagesWithUnsupportedTarget") @ParameterizedTest(name = "when target field type is `{0}`") - fun targetFieldHasUnsupportedType(message: KClass) { + fun whenTargetFieldHasUnsupportedType(message: KClass) { val descriptor = message.descriptor val error = assertCompilationFails(descriptor) val field = descriptor.field("target") @@ -55,7 +55,7 @@ internal class GoesPolicySpec : CompilationErrorTest() { @MethodSource("io.spine.validation.GoesPolicyTestEnv#messagesWithUnsupportedCompanion") @ParameterizedTest(name = "when companion's field type is `{0}`") - fun companionFieldHasUnsupportedType(message: KClass) { + fun whenCompanionFieldHasUnsupportedType(message: KClass) { val descriptor = message.descriptor val error = assertCompilationFails(descriptor) val field = descriptor.field("companion") @@ -63,7 +63,7 @@ internal class GoesPolicySpec : CompilationErrorTest() { } @Test - fun `the specified companion field does not exist`() { + fun `when the specified companion field does not exist`() { val message = GoesNonExistingCompanion.getDescriptor() val error = assertCompilationFails(message) val companion = "name" @@ -71,7 +71,7 @@ internal class GoesPolicySpec : CompilationErrorTest() { } @Test - fun `the field specified itself as its companion`() { + fun `when the field specified itself as its companion`() { val message = GoesSelfCompanion.getDescriptor() val error = assertCompilationFails(message) val field = message.field("name") diff --git a/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt index 57f9b005e8..3ec3e49a3d 100644 --- a/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt @@ -39,12 +39,12 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource -@DisplayName("`PatternPolicy` should reject the option when") +@DisplayName("`PatternPolicy` should reject the option") internal class PatternPolicySpec : CompilationErrorTest() { @MethodSource("io.spine.validation.PatternPolicyTestEnv#messagesWithUnsupportedTarget") @ParameterizedTest(name = "when target field type is `{0}`") - fun targetFieldHasUnsupportedType(message: KClass) { + fun whenTargetFieldHasUnsupportedType(message: KClass) { val descriptor = message.descriptor val error = assertCompilationFails(descriptor) val field = descriptor.field("value") @@ -56,7 +56,7 @@ internal class PatternPolicySpec : CompilationErrorTest() { } @Test - fun `the error message contains unsupported placeholders`() { + fun `when the error message contains unsupported placeholders`() { val message = PatternWithInvalidPlaceholders.getDescriptor() val error = assertCompilationFails(message) val field = message.field("value") diff --git a/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt index 8f79f20c58..9689be1757 100644 --- a/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/WhenPolicySpec.kt @@ -34,11 +34,11 @@ import io.spine.protodata.protobuf.field import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -@DisplayName("`WhenPolicy` should") +@DisplayName("`WhenPolicy` should reject") internal class WhenPolicySpec : CompilationErrorTest() { @Test - fun `reject option on a boolean field`() { + fun `option on a boolean field`() { val message = WhenBoolField.getDescriptor() val error = assertCompilationFails(message) val field = message.field("value") @@ -49,7 +49,7 @@ internal class WhenPolicySpec : CompilationErrorTest() { } } @Test - fun `reject option on an integer field`() { + fun `option on an integer field`() { val message = WhenInt32Field.getDescriptor() val error = assertCompilationFails(message) val field = message.field("value") @@ -60,7 +60,7 @@ internal class WhenPolicySpec : CompilationErrorTest() { } } @Test - fun `reject option on a string field`() { + fun `option on a string field`() { val message = WhenStringField.getDescriptor() val error = assertCompilationFails(message) val field = message.field("value") @@ -71,7 +71,7 @@ internal class WhenPolicySpec : CompilationErrorTest() { } } @Test - fun `the error message contains unsupported placeholders`() { + fun `the error message with unsupported placeholders`() { val message = WhenWithInvalidPlaceholders.getDescriptor() val error = assertCompilationFails(message) val field = message.field("value") diff --git a/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt b/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt index 9d23d25e79..59664a0481 100644 --- a/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt +++ b/model/src/testFixtures/kotlin/io/spine/validation/GoesPolicyTestEnv.kt @@ -36,7 +36,7 @@ import org.junit.jupiter.params.provider.Arguments.arguments object GoesPolicyTestEnv { /** - * Test data for [io.spine.validation.GoesPolicySpec.targetFieldHasUnsupportedType]. + * Test data for [io.spine.validation.GoesPolicySpec.whenTargetFieldHasUnsupportedType]. */ @JvmStatic fun messagesWithUnsupportedTarget() = listOf( @@ -56,7 +56,7 @@ object GoesPolicyTestEnv { ).map { arguments(named(it.first, it.second)) } /** - * Test data for [io.spine.validation.GoesPolicySpec.companionFieldHasUnsupportedType]. + * Test data for [io.spine.validation.GoesPolicySpec.whenCompanionFieldHasUnsupportedType]. */ @JvmStatic fun messagesWithUnsupportedCompanion() = listOf( From 267a23ba063c91f99d5e060298b1da12e0af549a Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 13:39:57 +0200 Subject: [PATCH 16/19] Bump copyright --- model/src/main/kotlin/io/spine/validation/ChoiceOption.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index 5b778b7073..a869fe028d 100644 --- a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024, TeamDev. All rights reserved. + * 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. From 846bb214a9daa3a2aaaf9c15760ab77cc3251fbe Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 13:40:25 +0200 Subject: [PATCH 17/19] Remove a redundant line --- .../testFixtures/proto/spine/validation/choice_option_spec.proto | 1 - 1 file changed, 1 deletion(-) diff --git a/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto b/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto index 88174b8a07..8f12574273 100644 --- a/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto +++ b/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto @@ -51,6 +51,5 @@ message ChoiceWithInvalidPlaceholders { string string_value = 2; bool bool_value = 3; - } } From d50b1a785ed753c57ce557ae1f034fad064c4b35 Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 14:30:21 +0200 Subject: [PATCH 18/19] Update wording regarding disabled options --- model/src/main/kotlin/io/spine/validation/ChoiceOption.kt | 6 +++++- model/src/main/kotlin/io/spine/validation/DistinctOption.kt | 5 +++-- model/src/main/kotlin/io/spine/validation/SetOnceOption.kt | 5 +++-- model/src/main/kotlin/io/spine/validation/ValidateOption.kt | 5 +++-- model/src/main/kotlin/io/spine/validation/WhenOption.kt | 4 +++- .../kotlin/io/spine/validation/required/RequiredOption.kt | 5 +++-- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index a869fe028d..c469bb56bd 100644 --- a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt @@ -57,7 +57,11 @@ import io.spine.validation.event.choiceOneofDiscovered * 1. The option has the `required` flag set to `true`. * 2. The error message does not contain unsupported placeholders. * - * If (1) is violated, the policy emits [NoReaction]. + * Violation of (1) means that the `(choice)` option is applied correctly, + * but effectively disabled. [ChoiceOneofDiscovered] is not emitted for + * disabled options. In this case, the policy emits [NoReaction] meaning + * that the option is ignored. + * * Violation of (2) leads to a compilation error. * * Note that unlike the `(required)` constraint, this option supports any field type. diff --git a/model/src/main/kotlin/io/spine/validation/DistinctOption.kt b/model/src/main/kotlin/io/spine/validation/DistinctOption.kt index f9502727e6..1487c70308 100644 --- a/model/src/main/kotlin/io/spine/validation/DistinctOption.kt +++ b/model/src/main/kotlin/io/spine/validation/DistinctOption.kt @@ -77,8 +77,9 @@ import io.spine.validation.event.ifHasDuplicatesOptionDiscovered * If (1) is violated, the policy reports a compilation error. * * Violation of (2) means that the `(distinct)` option is applied correctly, - * but disabled. In this case, the policy emits [NoReaction] because we - * actually have a non-distinct field, marked with `(distinct)`. + * but effectively disabled. [DistinctFieldDiscovered] is not emitted for + * disabled options. In this case, the policy emits [NoReaction] meaning + * that the option is ignored. */ internal class DistinctPolicy : Policy() { diff --git a/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt b/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt index 9821269d2a..a9cb3e9ae0 100644 --- a/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt @@ -77,8 +77,9 @@ import io.spine.validation.event.setOnceFieldDiscovered * If (1) is violated, the policy reports a compilation error. * * Violation of (2) means that the `(set_once)` option is applied correctly, - * but disabled. In this case, the policy emits [NoReaction] because we actually - * have a non-set-once field, marked with `(set_once)`. + * but effectively disabled. [SetOnceFieldDiscovered] is not emitted for + * disabled options. In this case, the policy emits [NoReaction] meaning + * that the option is ignored. */ internal class SetOncePolicy : Policy() { diff --git a/model/src/main/kotlin/io/spine/validation/ValidateOption.kt b/model/src/main/kotlin/io/spine/validation/ValidateOption.kt index 409bea0fd8..c7848588f3 100644 --- a/model/src/main/kotlin/io/spine/validation/ValidateOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ValidateOption.kt @@ -63,8 +63,9 @@ import io.spine.validation.event.validateFieldDiscovered * If (1) is violated, the policy reports a compilation error. * * Violation of (2) means that the `(validate)` option is applied correctly, - * but disabled. In this case, the policy emits [NoReaction] because we - * actually have a non-validated field, marked with `(validate)`. + * but effectively disabled. [ValidateFieldDiscovered] is not emitted for + * disabled options. In this case, the policy emits [NoReaction] meaning + * that the option is ignored. */ internal class ValidatePolicy : Policy() { diff --git a/model/src/main/kotlin/io/spine/validation/WhenOption.kt b/model/src/main/kotlin/io/spine/validation/WhenOption.kt index dc7c727c97..18d9d695ea 100644 --- a/model/src/main/kotlin/io/spine/validation/WhenOption.kt +++ b/model/src/main/kotlin/io/spine/validation/WhenOption.kt @@ -80,7 +80,9 @@ import io.spine.validation.TimeFieldType.TFT_UNKNOWN * If (1) or (2) is violated, the policy reports a compilation error. * * Violation of (3) means that the `(when)` option is applied correctly, - * but disabled. In this case, the policy emits [NoReaction]. + * but effectively disabled. [WhenFieldDiscovered] is not emitted for + * disabled options. In this case, the policy emits [NoReaction] meaning + * that the option is ignored. */ internal class WhenPolicy : Policy() { diff --git a/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt b/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt index 46ff7ea0bc..a09d6cfdf1 100644 --- a/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt +++ b/model/src/main/kotlin/io/spine/validation/required/RequiredOption.kt @@ -75,8 +75,9 @@ import io.spine.validation.required.RequiredFieldSupport.isSupported * If (1) is violated, the policy reports a compilation error. * * Violation of (2) means that the `(required)` option is applied correctly, - * but disabled. In this case, the policy emits [NoReaction] because we actually - * have a non-required field, marked with `(required)`. + * but effectively disabled. [RequiredFieldDiscovered] is not emitted for + * disabled options. In this case, the policy emits [NoReaction] meaning + * that the option is ignored. * * Note that this policy is responsible only for fields explicitly marked with * the validation option. There are other policies that handle implicitly From 90350e0fa65bd8e26acb05071b55d2a2824f60ae Mon Sep 17 00:00:00 2001 From: yevhenii-nadtochii Date: Tue, 29 Apr 2025 16:45:39 +0200 Subject: [PATCH 19/19] Sort constants within `SUPPORTED_PLACEHOLDERS` sets --- .../main/kotlin/io/spine/validation/ChoiceOption.kt | 5 ++++- .../main/kotlin/io/spine/validation/DistinctOption.kt | 9 +++++++-- .../src/main/kotlin/io/spine/validation/GoesOption.kt | 9 +++++++-- .../main/kotlin/io/spine/validation/PatternOption.kt | 10 ++++++++-- .../main/kotlin/io/spine/validation/SetOnceOption.kt | 9 +++++++-- .../src/main/kotlin/io/spine/validation/WhenOption.kt | 9 +++++++-- .../main/kotlin/io/spine/validation/bound/MaxOption.kt | 10 ++++++++-- .../main/kotlin/io/spine/validation/bound/MinOption.kt | 10 ++++++++-- .../kotlin/io/spine/validation/bound/RangeOption.kt | 9 +++++++-- .../io/spine/validation/required/RequireOption.kt | 5 ++++- .../io/spine/validation/required/RequiredOption.kt | 6 +++++- 11 files changed, 72 insertions(+), 19 deletions(-) diff --git a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index c469bb56bd..6551c08d00 100644 --- a/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt +++ b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt @@ -123,4 +123,7 @@ internal class ChoiceGroupView : View() { } } -private val SUPPORTED_PLACEHOLDERS = - setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, WHEN_IN) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + PARENT_TYPE, + WHEN_IN, +) diff --git a/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt b/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt index 1716499df4..350fcd17b8 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/MaxOption.kt @@ -105,5 +105,11 @@ internal class MaxFieldView : View() { } } -private val SUPPORTED_PLACEHOLDERS = - setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, MAX_VALUE, MAX_OPERATOR) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + MAX_OPERATOR, + MAX_VALUE, + PARENT_TYPE, +) diff --git a/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt b/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt index d4cac12034..b59f4bf1b4 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/MinOption.kt @@ -105,5 +105,11 @@ internal class MinFieldView : View() { } } -private val SUPPORTED_PLACEHOLDERS = - setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, MIN_VALUE, MIN_OPERATOR) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + MIN_OPERATOR, + MIN_VALUE, + PARENT_TYPE, +) diff --git a/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt b/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt index 129b9a5587..49665522f0 100644 --- a/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt +++ b/model/src/main/kotlin/io/spine/validation/bound/RangeOption.kt @@ -179,5 +179,10 @@ private fun RangeContext.checkRelation(lower: KotlinNumericBound, upper: KotlinN private val DELIMITER = Regex("""(?<=\d)\s?\.\.\s?(?=[\d-+])""") -private val SUPPORTED_PLACEHOLDERS = - setOf(FIELD_PATH, FIELD_VALUE, FIELD_TYPE, PARENT_TYPE, RANGE_VALUE) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + PARENT_TYPE, + RANGE_VALUE, +) diff --git a/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt b/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt index 9c26dfc309..c572a88d60 100644 --- a/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt +++ b/model/src/main/kotlin/io/spine/validation/required/RequireOption.kt @@ -100,4 +100,7 @@ internal class RequireMessageView : View