Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
44f253e
Add `MessageValidator` SPI interface
May 5, 2025
a38cb3b
Merge branch 'master' into message-validator
May 6, 2025
12e9905
Bump the version -> `2.0.0-SNAPSHOT.333`
May 6, 2025
eed6eea
Update reports
May 6, 2025
ceb4763
Introduce `@Validator` annotation
May 6, 2025
a25515a
Remove an empty line
May 6, 2025
8f473c7
Implement a test stub
May 6, 2025
1e17875
Make `MessageValidator` return a list of violations
May 6, 2025
dc2d9e0
Create `:java-tests:validator` module
May 6, 2025
afe5c2f
Implement `FileDescriptorSetValidator`
May 6, 2025
b1a232f
Create `:java-ksp` module
May 6, 2025
11a9b7e
Try to discover custom validators in `JavaValidationPlugin`
May 6, 2025
8fa81fc
Comment out task dependencies
May 13, 2025
5adc510
Suppress Detekt warnings
May 13, 2025
4baa44c
Make `kspKotlin` run before ProtoData
May 14, 2025
767b8e3
Implement `ValidatorRegistry`
May 14, 2025
1c2fbe6
Prototype `ValidatorProcessor` and `ValidatorRegistry` coordination
May 14, 2025
c6700d4
Remove `ValidatorRegistry` class
May 15, 2025
6d7356d
Merge branch 'master' into message-validator
May 21, 2025
af9a2f1
Add a new shortcut for declaring dependencies
May 21, 2025
e5b719c
Declare KSP dependency using new API
May 21, 2025
d369e60
Assert `WhenFactory` respectively
May 21, 2025
3e44ac1
Bump the version -> `2.0.0-SNAPSHOT.342`
May 22, 2025
42b6caf
Update reports
May 22, 2025
68e1724
Make sure KSP always runs
May 22, 2025
47662fc
Merge branch 'master' into message-validator
May 22, 2025
77acacc
Bump the version -> `2.0.0-SNAPSHOT.342`
May 22, 2025
b661a4f
Have discovered validators in ProtoData using a plain file
May 22, 2025
7c94b89
Pass `customValidators` to `JavaValidationRenderer`
May 23, 2025
49d5536
Rename the constant
May 23, 2025
36a67d4
Make `MessageValidator` accept parental info
May 23, 2025
433c0e2
Make `MessageValidator` accept `fieldPath` and `typeName`
May 23, 2025
3447734
Map message class to validator class during discovering
May 23, 2025
e5ecf5d
Implement `ValidatorGenerator`
May 23, 2025
8f9da03
Leave `todo`s for `@Validator` annotation
May 23, 2025
5b9257b
Implement integration tests for validators
May 23, 2025
5553e21
Remove `ValidatorProcessorSpec`
May 23, 2025
6ba9dcd
Make the processor always create an output file
May 23, 2025
e611ab8
Do not throw if there are no validators
May 23, 2025
5850f30
Create `ValidatorViolation`
May 26, 2025
b510883
Temporary commit specs to a todo comment
May 26, 2025
62c1cd0
Adjust validators to the new interface
May 26, 2025
80b5196
Implement more test cases
May 26, 2025
9028bb2
Make the base class contain all necessary properties
May 26, 2025
edcbd39
Handle `ValidatorViolation` in the generated code
May 26, 2025
346e37d
Allow validator codegen for `repeated` and `map` fields
May 26, 2025
3e122cf
Support `repeated` and `map` fields for validators
May 26, 2025
3eef81f
Rename the private method to avoid confusion
May 26, 2025
d1f3bc1
Make test validator check exact instance
May 27, 2025
c81f246
Cover repeated and map fields with tests
May 27, 2025
5917f30
Clean up imports
May 27, 2025
3032d41
Check validators declared only for external messages
May 27, 2025
82bbea1
Test external messages from dependencies
May 27, 2025
a9de0a9
Reference artifacts with extension
May 28, 2025
18fcea4
Give a usage example
May 28, 2025
940c31c
Implement `EarphonesValidator`
May 28, 2025
a5d686f
Implement tests for messages from dependencies
May 28, 2025
80db0c7
Move `DetectedViolation` to a separate file
May 28, 2025
f06758f
Document `MessageValidator`
May 28, 2025
03f9fdf
Rename the object
May 28, 2025
a25d66b
Update docs to the `@Validator` annotation
May 28, 2025
772d548
Bump `KotlinCompileTesting`
May 29, 2025
e8b88b4
Set up KSP testing
May 29, 2025
183bdfe
Force Kotlin embeddable and KSP
May 29, 2025
05bfe8f
Assert the generated file
May 29, 2025
a513a2a
Extract common code of positive tests
May 29, 2025
ef1a3f7
Implement tests for KSP processor
May 29, 2025
b9027f2
Suppress `ReturnCount`
May 29, 2025
c16d89a
Proofread docs
May 30, 2025
e1fe3dc
Refactor the assertion methods
May 30, 2025
3cb0ee0
Shorten the list of required dependencies
May 30, 2025
131512c
Remove no longer needed dependency
May 30, 2025
84ab928
Proofread docs
May 30, 2025
5548192
Move `ValidatorGenerator` to the `generate` package
May 30, 2025
4015a23
Remain only the necessary task dependencies
May 30, 2025
ad0a8fc
Update reports
May 30, 2025
fe7991d
Explain the difference between local and external messages
May 30, 2025
9fbe3f0
Remove duplicate mentioning of the restriction
May 30, 2025
0a712b3
Capitalize the library name
May 30, 2025
5aeba49
Rename the section
May 30, 2025
eff29ff
Get rid of `custom` bound
May 30, 2025
b0f1766
Introduce the `Problem` section
May 30, 2025
4f65755
Avoid `embedded` in docs to the validator
May 30, 2025
34f7049
Move the resolving logic to the `DiscoveredValidators` object
May 30, 2025
ed00afe
Enhance docs
May 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion buildSrc/src/main/kotlin/io/spine/dependency/Dependency.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ abstract class Dependency {
/**
* The [modules] given with the [version].
*/
final val artifacts: Map<String, String> by lazy {
val artifacts: Map<String, String> by lazy {
modules.associateWith { "$it:$version" }
}

Expand Down Expand Up @@ -114,3 +114,19 @@ private fun ResolutionStrategy.forceWithLogging(
force(artifact)
project.log { "Forced the version of `$artifact` in " + configuration.diagSuffix(project) }
}

/**
* Obtains full Maven coordinates for the requested [module].
*
* This extension allows referencing properties of the [Dependency],
* upon which it is invoked.
*
* An example usage:
*
* ```
* // Supposing there is `Ksp.symbolProcessingApi: String` property declared.
* Ksp.artifact { symbolProcessingApi }
* ```
*/
fun <T : Dependency> T.artifact(module: T.() -> String): String =
artifact(module())
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ package io.spine.dependency.test
*/
@Suppress("unused", "ConstPropertyName")
object KotlinCompileTesting {
private const val version = "0.7.0"
private const val version = "0.7.1"
private const val group = "dev.zacsweers.kctfork"
const val libCore = "$group:core:$version"
const val libKsp = "$group:ksp:$version"
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/kotlin/module.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.spine.dependency.boms.BomsPlugin
import io.spine.dependency.build.Dokka
import io.spine.dependency.build.ErrorProne
import io.spine.dependency.build.JSpecify
import io.spine.dependency.build.Ksp
import io.spine.dependency.lib.Grpc
import io.spine.dependency.lib.Kotlin
import io.spine.dependency.lib.Protobuf
Expand Down Expand Up @@ -144,8 +145,10 @@ fun Module.forceConfigurations() {
all {
resolutionStrategy {
Grpc.forceArtifacts(project, this@all, this@resolutionStrategy)
Ksp.forceArtifacts(project, this@all, this@resolutionStrategy)
force(
Kotlin.bom,
Kotlin.Compiler.embeddable,
Reflect.lib,
Base.lib,
Protobuf.compiler,
Expand Down
4,095 changes: 3,502 additions & 593 deletions dependencies.md

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,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.
*/

package io.spine.validation.api

import io.spine.annotation.Internal
import java.io.File

/**
* 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"

/**
* Resolves the path to the file containing discovered message validators.
*
* @param kspOutputDirectory The path to the KSP output.
*/
public fun resolve(kspOutputDirectory: File): File = kspOutputDirectory
.resolve("resources")
.resolve(RESOURCES_LOCATION)
}
142 changes: 142 additions & 0 deletions java-api/src/main/kotlin/io/spine/validation/api/MessageValidator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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 validator for an external Protobuf message of type [M].
*
* This interface enforces validation rules for `Message`s whose Java/Kotlin classes
* are already generated by third parties, in case they are used in the Protobuf
* codebase to which the Validation library is applied.
*
* ## Problem
*
* Java/Kotlin libraries that use Protobuf messages often distribute both the `.proto`
* definitions and the compiled class files (`.class`) for these messages.
* As these classes are pre-generated, consumers cannot modify their underlying
* `.proto` files to define validation constraints and the Validation library
* cannot use code generation to enforce the constraints.
*
* Thus, the library effectively deals with the two types of messages:
*
* 1. **Local messages** are message types for which end-users generate and control
* the Java/Kotlin classes by compiling the corresponding `.proto` definitions
* within their own codebase. For such messages, the Validation library allows
* declaring validation constraints and enforces them with the generated code.
*
* 2. **External messages** are message types for which end-users do not generate or control
* the Java/Kotlin classes. Because the classes are already generated, users cannot modify
* the underlying `.proto` definitions and add validation options at compile time.
*
* ## Validation of external messages
*
* The Validation library provides a mechanism that allows validating of
* the external messages, **which are used for fields within local messages**.
* Implement this interface and annotate the implementing class with
* the [@Validator][Validator] annotation, specifying the message type to validate.
*
* For each field of type [M] within any local message, the library will invoke
* the [MessageValidator.validate] method when validating the local message.
*
* The following Protobuf field types are supported:
*
* 1. Singular fields of type [M].
* 2. Repeated field of type [M].
* 3. Map field with values of type [M].
*
* 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.
* }
* }
* ```
*
* Please note that standalone instances of [M] and fields of [M] type that occur in
* other external messages **will not be validated**.
*
* Consider the following example:
*
* ```proto
* // Brings the `Earphones` message from dependencies.
* // Suppose we don't control the generated code of declarations from this file.
* import "earphones.proto";
*
* // A locally-declared message.
* message WorkingSetup {
*
* // The field of external message type.
* Earphones earphones = 1;
* }
* ```
*
* Supposing that the Validation library applied to the module where both `WorkingSetup`
* and `EarphonesValidator` classes are declared, then the generated code of `WorkingSetup`
* will apple the validator to each instance passed to the `WorkingSetup.earphones` field.
*
* Please note that the following use cases are NOT supported and will lead to an error:
*
* 1) Declaring a validator for a local message is prohibited. Only external messages are
* allowed to have a validator. Use built-in or custom validation options to declare
* constraints for local messages.
* 2) Declaring multiple validators for the same message type is prohibited. The library
* scans the module’s classpath to discover validators, and expects exactly one validator
* per message type.
*
* ## Implementation
*
* The interface offers flexible validation strategies. Implementations can choose to
* validate a particular field, 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 library converts
* [DetectedViolation] to a [ConstraintViolation][io.spine.validate.ConstraintViolation].
* Returning of an empty list of violations means that the given 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.
*
* @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>
}
55 changes: 55 additions & 0 deletions java-api/src/main/kotlin/io/spine/validation/api/Validator.kt
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>
)
Loading
Loading