-
Notifications
You must be signed in to change notification settings - Fork 0
Support custom message validators #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 76 commits
44f253e
a38cb3b
12e9905
eed6eea
ceb4763
a25515a
8f473c7
1e17875
dc2d9e0
afe5c2f
b1a232f
11a9b7e
8fa81fc
5adc510
4baa44c
767b8e3
1c2fbe6
c6700d4
6d7356d
af9a2f1
e5b719c
d369e60
3e44ac1
42b6caf
68e1724
47662fc
77acacc
b661a4f
7c94b89
49d5536
36a67d4
433c0e2
3447734
e5ecf5d
8f9da03
5b9257b
5553e21
6ba9dcd
e611ab8
5850f30
b510883
62c1cd0
80b5196
9028bb2
edcbd39
346e37d
3e122cf
3eef81f
d1f3bc1
c81f246
5917f30
3032d41
82bbea1
a9de0a9
18fcea4
940c31c
a5d686f
80db0c7
f06758f
03f9fdf
a25d66b
772d548
e8b88b4
183bdfe
05bfe8f
a513a2a
ef1a3f7
b9027f2
c16d89a
e1fe3dc
3cb0ee0
131512c
84ab928
5548192
4015a23
ad0a8fc
fe7991d
9fbe3f0
0a712b3
5aeba49
eff29ff
b0f1766
4f65755
34f7049
ed00afe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /* | ||
| * 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.api | ||
|
|
||
| import io.spine.base.FieldPath | ||
| import io.spine.validate.TemplateString | ||
|
|
||
| /** | ||
| * Abstract base for violations detected by [MessageValidator]s. | ||
| * | ||
| * @param message The error message describing the violation. | ||
| * @param fieldPath The path to the field where the violation occurred, if applicable. | ||
| * @param fieldValue The field value that caused the violation, if any. | ||
| */ | ||
| public abstract class DetectedViolation( | ||
| public val message: TemplateString, | ||
| public val fieldPath: FieldPath?, | ||
| public val fieldValue: Any?, | ||
| ) | ||
|
|
||
| /** | ||
| * A violation tied to a specific field in a message. | ||
| * | ||
| * @param message The error message describing the violation. | ||
| * @param fieldPath The path to the field where the violation occurred. | ||
| * @param fieldValue The field value that caused the violation, if any. | ||
| */ | ||
| public class FieldViolation( | ||
| message: TemplateString, | ||
| fieldPath: FieldPath, | ||
| fieldValue: Any? = null, | ||
| ) : DetectedViolation(message, fieldPath, fieldValue) | ||
|
|
||
| /** | ||
| * A violation related to the message level (not tied to a specific field). | ||
| * | ||
| * @param message The error message describing the violation. | ||
| */ | ||
| public class MessageViolation( | ||
| message: TemplateString | ||
| ) : DetectedViolation(message, fieldPath = null, fieldValue = null) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /* | ||
| * 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.api | ||
|
|
||
| import io.spine.annotation.Internal | ||
|
|
||
| /** | ||
| * Holds a path to a file with the discovered validators. | ||
| * | ||
| * The KSP processor generates a resource file using this path. | ||
| * Then, the Java codegen plugin picks up this file. | ||
| */ | ||
| @Internal | ||
| public object DiscoveredValidators { | ||
|
|
||
| /** | ||
| * The path to the file with the discovered message validators. | ||
| * | ||
| * The path is relative to the output directory of the KSP processor. | ||
| */ | ||
| public const val RESOURCES_LOCATION: String = "spine/validation/message-validators" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /* | ||
| * 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.api | ||
|
|
||
| import com.google.protobuf.Message | ||
| import io.spine.annotation.SPI | ||
|
|
||
| /** | ||
| * A custom validator for Protobuf messages of type [M] that are defined externally. | ||
| * | ||
| * External messages are those that come from dependencies. | ||
| * Protobuf [well-known](https://protobuf.dev/reference/protobuf/google.protobuf/) | ||
| * messages should also be considered external. For such messages, it is impossible | ||
| * to declare validation constraints using the validation options because there's | ||
| * no access to their proto definitions. | ||
| * | ||
armiol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * To be able to validate external messages, one must implement this interface | ||
| * and annotate the implementing class with the [Validator] annotation, specifying | ||
| * the type of the message to validate. | ||
| * | ||
| * An example of the validator declaration for the `Earphones` message: | ||
| * | ||
| * ```kotlin | ||
| * @Validator(Earphones::class) | ||
| * public class EarphonesValidator : MessageValidator<Earphones> { | ||
| * public override fun validate(message: Earphones): List<DetectedViolation> { | ||
| * return emptyList() // Always valid. | ||
| * } | ||
| * } | ||
| * ``` | ||
| * | ||
| * ## Applicability | ||
| * | ||
| * A validator is applied only to the local messages of the module it is declared in. | ||
|
||
| * For each local message that has a field of type [M], the validation library | ||
| * will invoke a validator when validating that message. | ||
| * | ||
| * The following field types are supported: | ||
| * | ||
| * 1. Singular fields of type [M]. | ||
| * 2. Repeated field of type [M]. | ||
| * 3. Map field with values of type [M]. | ||
| * | ||
| * Please note that standalone instances of [M] and fields of [M] type that occur in | ||
| * external messages will not be validated using the validator. | ||
| * | ||
| * Consider the following example: | ||
| * | ||
| * ```proto | ||
| * import "earphones.proto"; // Brings the `Earphones` message from dependencies. | ||
| * | ||
| * // A locally-declared message. | ||
armiol marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| * message WorkingSetup { | ||
| * | ||
| * // The field of external message type. | ||
| * Earphones earphones = 1; | ||
| * } | ||
| * ``` | ||
| * | ||
| * Supposing that `WorkingSetup` and `EarphonesValidator` are declared within | ||
| * the same module, then every instance of `WorkingSetup.earphones` will be validated | ||
| * with the validator. | ||
| * | ||
| * Also note the restrictions: | ||
| * | ||
| * - Standalone instantiations of `Earphones` will not be validated. | ||
| * - External messages that use `Earphones` as a field will not be validated. | ||
| * | ||
| * ## Implementation | ||
| * | ||
| * The message validator does not have restrictions upon how exactly the message | ||
|
||
| * must be validated. It can validate a particular field or several fields, | ||
| * the whole message instance (for example, checking the field relations) and | ||
| * perform a deep validation. | ||
| * | ||
| * It is a responsibility of the validator to provide the correct instances | ||
| * of [DetectedViolation]. Before reporting to the user, the validation library | ||
| * converts [DetectedViolation] to a [ConstraintViolation][io.spine.validate.ConstraintViolation]. | ||
| * Returning of an empty list of violations means that the message is valid. | ||
| * | ||
| * Please keep in mind that for each invocation a new instance of [MessageValidator] | ||
| * is created. Every implementation of [MessageValidator] must have a public, | ||
| * no-args constructor. | ||
| * | ||
| * ## Restrictions | ||
| * | ||
| * A [MessageValidator] will be rejected by the validation library in the following cases: | ||
| * | ||
| * 1) It is used to validate a local message. Only external messages are allowed | ||
| * to have a validator. | ||
| * 2) There already exists a validator for the validated message type. Having several | ||
| * validators for the same message type is prohibited. | ||
| * | ||
| * @param M the type of Protobuf [Message] being validated. | ||
| */ | ||
| @SPI | ||
| public interface MessageValidator<M : Message> { | ||
|
|
||
| /** | ||
| * Validates the given [message]. | ||
| * | ||
| * @return the detected violations or empty list. | ||
| */ | ||
| public fun validate(message: M): List<DetectedViolation> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| */ | ||
|
|
||
| package io.spine.validation.api | ||
|
|
||
| import com.google.protobuf.Message | ||
| import kotlin.annotation.AnnotationRetention.SOURCE | ||
| import kotlin.annotation.AnnotationTarget.CLASS | ||
| import kotlin.reflect.KClass | ||
|
|
||
| /** | ||
| * Marks the class as a message validator. | ||
| * | ||
| * Applying this annotation to an implementation of [MessageValidator] | ||
| * makes the class visible to the validation library. | ||
| * | ||
| * Please note that the following requirements are imposed to the marked class: | ||
| * | ||
| * 1. The class must implement the [MessageValidator] interface. | ||
| * 2. The class must have a public, no-args constructor. | ||
| * 3. The class cannot be `inner`, but nested classes are allowed. | ||
| * 4. The message type of [Validator.value] and [MessageValidator] must match. | ||
| */ | ||
| @Target(CLASS) | ||
| @Retention(SOURCE) | ||
| public annotation class Validator( | ||
|
|
||
| /** | ||
| * The class of the validated external message. | ||
| */ | ||
| val value: KClass<out Message> | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We consider "external" those
Messagedefinitions for which our end-users do not generate the Java/Kotlin code.The definition you give is just misleading because end-users can generate code for any Proto type, including those coming from any third-party dependency. Many libraries come with their own generated and compiled Java code for their Proto definitions (such as Google Cloud libraries). And it is pretty difficult to generate another version of Java code and still make the libraries work properly.
I think the beginning (maybe even including the first paragraph) should be updated accordingly. Let's consider the readers much less prepared.