diff --git a/dependencies.md b/dependencies.md index 3e4519d35b..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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt b/model/src/main/kotlin/io/spine/validation/ChoiceOption.kt index 87d6cf269e..6551c08d00 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. @@ -43,15 +43,26 @@ 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 /** * 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]. + * 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. + * + * 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. * Protobuf encodes a non-set value as a special case, allowing for checking whether @@ -64,13 +75,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 +122,8 @@ internal class ChoiceGroupView : View() { @@ -129,7 +130,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 +171,10 @@ 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) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_DUPLICATES, + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + PARENT_TYPE, +) diff --git a/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholder.kt index c48fd6c40b..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. * @@ -76,25 +74,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..c0d6ea4acc --- /dev/null +++ b/model/src/main/kotlin/io/spine/validation/ErrorPlaceholders.kt @@ -0,0 +1,145 @@ +/* + * 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.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 [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 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. + */ +internal fun String.checkPlaceholders( + supported: Set, + message: MessageType, + file: File, + option: String +) = 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 [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 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. + */ +internal fun String.checkPlaceholders( + supported: Set, + oneof: OneofGroup, + file: File, + option: String +) = 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 [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 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. + */ +internal fun String.checkPlaceholders( + 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] placeholders, 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, 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 }}`." + } +} + +/** + * 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 1f72278409..0800a852f6 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 } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, GOES) + return goesFieldDiscovered { id = field.ref errorMessage = message @@ -147,3 +155,11 @@ private fun FieldType.isSupported(): Boolean = private val SUPPORTED_PRIMITIVES = listOf( TYPE_STRING, TYPE_BYTES ) + +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + GOES_COMPANION, + PARENT_TYPE, +) diff --git a/model/src/main/kotlin/io/spine/validation/PatternOption.kt b/model/src/main/kotlin/io/spine/validation/PatternOption.kt index ae3004e43a..a3fb9b9a83 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,12 @@ public val FieldType.isRepeatedString: Boolean */ public val FieldType.isSingularString: Boolean get() = primitive == TYPE_STRING + +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + FIELD_VALUE, + PARENT_TYPE, + REGEX_MODIFIERS, + REGEX_PATTERN, +) diff --git a/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt b/model/src/main/kotlin/io/spine/validation/SetOnceOption.kt index ddfe1970ec..f5135cff45 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() { @@ -129,7 +130,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 +172,10 @@ 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) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_PROPOSED_VALUE, + FIELD_TYPE, + FIELD_VALUE, + PARENT_TYPE, +) 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 d4cfd53975..f865f9d7df 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 @@ -69,12 +74,15 @@ 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, - * but disabled. In this case, the policy emits [NoReaction]. + * Violation of (3) means that the `(when)` option is applied correctly, + * 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() { @@ -94,6 +102,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 +166,11 @@ internal class WhenFieldView : View() { type = e.type } } + +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 022700f19d..350fcd17b8 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,12 @@ internal class MaxFieldView : View() { file = e.file } } + +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 ddd0404bbd..b59f4bf1b4 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,12 @@ internal class MinFieldView : View() { file = e.file } } + +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 c8af09592f..49665522f0 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. @@ -63,6 +69,7 @@ import io.spine.validation.bound.event.rangeFieldDiscovered * 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. * @@ -99,6 +106,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 +178,11 @@ private fun RangeContext.checkRelation(lower: KotlinNumericBound, upper: KotlinN } private val DELIMITER = Regex("""(?<=\d)\s?\.\.\s?(?=[\d-+])""") + +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 eb225feaff..c572a88d60 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,8 @@ internal class RequireMessageView : View() { 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 +151,8 @@ 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) +private val SUPPORTED_PLACEHOLDERS = setOf( + FIELD_PATH, + FIELD_TYPE, + PARENT_TYPE, +) 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/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/GoesPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/GoesPolicySpec.kt index d53695f385..7edb092950 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 @@ -62,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" @@ -70,12 +71,25 @@ 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") 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/test/kotlin/io/spine/validation/PatternPolicySpec.kt b/model/src/test/kotlin/io/spine/validation/PatternPolicySpec.kt new file mode 100644 index 0000000000..3ec3e49a3d --- /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") +internal class PatternPolicySpec : CompilationErrorTest() { + + @MethodSource("io.spine.validation.PatternPolicyTestEnv#messagesWithUnsupportedTarget") + @ParameterizedTest(name = "when target field type is `{0}`") + fun whenTargetFieldHasUnsupportedType(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/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/test/kotlin/io/spine/validation/RequirePolicySpec.kt b/model/src/test/kotlin/io/spine/validation/RequirePolicySpec.kt index 6d5b9537ba..e49990bfe3 100644 --- a/model/src/test/kotlin/io/spine/validation/RequirePolicySpec.kt +++ b/model/src/test/kotlin/io/spine/validation/RequirePolicySpec.kt @@ -27,6 +27,7 @@ package io.spine.validation 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 @@ -108,6 +109,18 @@ internal class RequirePolicySpec : CompilationErrorTest() { shouldContain(secondFieldGroup) } } + + @Test + fun `reject the error message contains unsupported placeholders`() { + val message = RequireWithInvalidPlaceholders.getDescriptor() + val error = assertCompilationFails(message) + error.message.run { + shouldContain(message.fullName) + shouldContain(REQUIRE) + shouldContain("unsupported placeholders") + shouldInclude("[message.name]") + } + } } private fun unsupportedFieldType(field: Field) = 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]") } } 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..9689be1757 --- /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 reject") +internal class WhenPolicySpec : CompilationErrorTest() { + + @Test + fun `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 `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 `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 with 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/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/choice_option_spec.proto b/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto new file mode 100644 index 0000000000..8f12574273 --- /dev/null +++ b/model/src/testFixtures/proto/spine/validation/choice_option_spec.proto @@ -0,0 +1,55 @@ +/* + * 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; + } +} 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..c37daf01ea --- /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." + ]; +} 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." + ]; +} 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." + ]; +} diff --git a/model/src/testFixtures/proto/spine/validation/require_option_spec.proto b/model/src/testFixtures/proto/spine/validation/require_option_spec.proto index ba4d38907c..ee783893d3 100644 --- a/model/src/testFixtures/proto/spine/validation/require_option_spec.proto +++ b/model/src/testFixtures/proto/spine/validation/require_option_spec.proto @@ -98,3 +98,13 @@ message RequireDuplicateGroups { string field2 = 2; string field3 = 3; } + +// Provides a message marked with the `(require)` option that specifies a custom error +// message using the placeholders not supported by the option. +message RequireWithInvalidPlaceholders { + option (require) = { + fields: "value", + error_msg: "At least one of `${require.fields}` must be set in `${message.name}`." + }; + string value = 1; +} 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}`." + }]; +} 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 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")