Skip to content

Make the Java validation extendable #215

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

Merged
merged 53 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
63fad62
Implement `JavaValidationExtension` interface
yevhenii-nadtochii Apr 24, 2025
5f14ddf
Address visibility issues
yevhenii-nadtochii Apr 24, 2025
769823c
Remove a trailing line
yevhenii-nadtochii Apr 24, 2025
82bf6f4
Make `JavaValidationPlugin` load `JavaValidationExtension`
yevhenii-nadtochii Apr 24, 2025
c5c9bf8
Re-implement `CurrencyOption`
yevhenii-nadtochii Apr 24, 2025
08b5be1
Make `templateString()` non-typed
yevhenii-nadtochii Apr 24, 2025
fa705d1
Make injector variables public
yevhenii-nadtochii Apr 24, 2025
340a462
Mark more entities as `public` to allow for custom generator
yevhenii-nadtochii Apr 24, 2025
49494c7
Propagate `Querying` with the `OptionGenerator` class
yevhenii-nadtochii Apr 24, 2025
fadd5db
Implement `MoneyValidationExtension`
yevhenii-nadtochii Apr 24, 2025
3cdfbf9
Remove `Querying` from `CurrencyGenerator`
yevhenii-nadtochii Apr 24, 2025
96af167
Bump the library version -> `2.0.0-SNAPSHOT.315`
yevhenii-nadtochii Apr 24, 2025
b7ce31d
Fix broken imports
yevhenii-nadtochii Apr 24, 2025
e2dbcb1
Align `CurrencyGenerator` with its test cases
yevhenii-nadtochii Apr 24, 2025
da23133
Do not pass `MoneyValidationPlugin` anymore
yevhenii-nadtochii Apr 24, 2025
9ff029c
Update reports
yevhenii-nadtochii Apr 24, 2025
295d61f
Bring docs to up-to-date
yevhenii-nadtochii Apr 24, 2025
ba324c3
Enhance naming within the custom generator
yevhenii-nadtochii Apr 24, 2025
fc7ec2a
Shorten docs
yevhenii-nadtochii Apr 24, 2025
e694eed
Remove `todo`
yevhenii-nadtochii Apr 24, 2025
7d19e94
Remove unused import
yevhenii-nadtochii Apr 24, 2025
81c0a2e
Rename `JavaValidationExtension` to `CustomOptionSupport`
yevhenii-nadtochii Apr 24, 2025
9aac6ed
Rename `CustomOptionSupport` to `CustomOption`
yevhenii-nadtochii Apr 25, 2025
11cf9cd
Bump copyright
yevhenii-nadtochii Apr 29, 2025
1efd327
Make `CustomOption` accept multiple views and policies
yevhenii-nadtochii Apr 29, 2025
523a810
Re-format `ValidationPlugin`
yevhenii-nadtochii Apr 29, 2025
5d514ed
Use `@Internal` annotation
yevhenii-nadtochii Apr 29, 2025
07f9051
Merge branch 'master' into make-codegen-extendable
yevhenii-nadtochii Apr 29, 2025
194d5c8
Bump the version -> `2.0.0-SNAPSHOT.317`
yevhenii-nadtochii Apr 29, 2025
18f719a
Make post-merge fixes
yevhenii-nadtochii Apr 29, 2025
0e6e716
Update reports
yevhenii-nadtochii Apr 29, 2025
cca0ee0
Rename `CustomOption` -> `ValidationOption`
yevhenii-nadtochii Apr 29, 2025
eafd3d6
Merge branch 'remove-rule-classes' into make-codegen-extendable
yevhenii-nadtochii Apr 30, 2025
2626ced
Provide built-in generators with a separate function
yevhenii-nadtochii Apr 30, 2025
fe0151d
Enable back extension tests
yevhenii-nadtochii Apr 30, 2025
4816e45
Bump the version -> `2.0.0-SNAPSHOT.318`
yevhenii-nadtochii Apr 30, 2025
d44e036
Update reports
yevhenii-nadtochii Apr 30, 2025
408e7b8
Init the `:java-api` mode
yevhenii-nadtochii Apr 30, 2025
cd882fa
Merge branch 'remove-rule-classes' into make-codegen-extendable
yevhenii-nadtochii Apr 30, 2025
5cb9719
Move public declarations from `:model` and `:java` to `:java-api`
yevhenii-nadtochii Apr 30, 2025
5fdb8a4
Remove `@Internal` from the `:model`
yevhenii-nadtochii Apr 30, 2025
9132b77
Move `mangled()` to the API module
yevhenii-nadtochii Apr 30, 2025
882ea9d
Update dependency reports
yevhenii-nadtochii Apr 30, 2025
0ba900f
Merge branch 'master' into make-codegen-extendable
yevhenii-nadtochii May 2, 2025
406f039
Document `builtInGenerators()`
yevhenii-nadtochii May 2, 2025
0269119
Describe the difference between two same-named methods
yevhenii-nadtochii May 2, 2025
36f031a
Extract `option` package in `:model`
yevhenii-nadtochii May 2, 2025
f2fa894
Expand docs to `OPTION_NAME` constant
yevhenii-nadtochii May 2, 2025
04b184f
Leave a todo with issue line
yevhenii-nadtochii May 2, 2025
80e73ce
Proofread public docs
yevhenii-nadtochii May 2, 2025
145bd3f
Make `GoesGenerator` us `MessageScope`
yevhenii-nadtochii May 2, 2025
41a8c3d
Update build time
yevhenii-nadtochii May 2, 2025
1611c3e
Roll back packages
yevhenii-nadtochii May 2, 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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ spinePublishing {
"java",
"java-bundle",
"java-runtime",
"java-api"
)
modulesWithCustomPublishing = setOf(
"java-bundle",
Expand Down
3,229 changes: 3,022 additions & 207 deletions dependencies.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -24,12 +24,9 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.validation.test
import io.spine.dependency.local.ProtoData

import io.spine.protodata.plugin.Plugin

@Suppress("unused") // Accessed reflectively by ProtoData.
public class MoneyValidationPlugin : Plugin(
viewRepositories = setOf(CurrencyTypeView.Repo()),
policies = setOf(CurrencyValidationPolicy())
)
dependencies {
api(ProtoData.java)
api(project(":proto:context"))
}
46 changes: 46 additions & 0 deletions java-api/src/main/kotlin/io/spine/validation/api/OptionName.kt
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

/**
* Path to the name field in `*OptionDiscovered` events.
*
* The main purpose of this constant is to set the field path when
* filtering incoming events by the option name. It is usually used
* in a pair with the constant containing the option name.
*
* An example of event filtering in a policy:
*
* ```
* @React
* override fun whenever(
* @External @Where(field = OPTION_NAME, equals = REQUIRED)
* event: FieldOptionDiscovered,
* ): EitherOf2<RequiredFieldDiscovered, NoReaction> {
* ```
*/
public const val OPTION_NAME: String = "option.name"
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.validation
package io.spine.validation.api

import com.google.protobuf.ByteString
import io.spine.protodata.ast.Cardinality.CARDINALITY_LIST
Expand Down Expand Up @@ -56,6 +56,8 @@ import io.spine.string.shortly
* This class does not instantiate default values of Protobuf messages. It merely creates
* instances of [Value] which represent values of Protobuf message fields, which are not set.
*/
// TODO:2025-03-12:yevhenii.nadtochii: `EmptyFieldCheck` should not need this object.
// See issue: https://github.com/SpineEventEngine/validation/issues/199
public object UnsetValue {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.SPI
import io.spine.protodata.plugin.Policy
import io.spine.protodata.plugin.View
import io.spine.validation.api.generate.OptionGenerator

/**
* Extends the Java validation library with the custom validation option.
*/
@SPI
public interface ValidationOption {

/**
* The [policies][Policy] added by the option.
*/
public val policy: Set<Policy<*>>

/**
* The [views][View] added by the option.
*/
public val view: Set<Class<out View<*, *, *>>>

/**
* The option [generator][OptionGenerator] for Java.
*/
public val generator: OptionGenerator
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package io.spine.validation.java.expression
package io.spine.validation.api.expression

import io.spine.protodata.java.AnnotatedClassName
import io.spine.protodata.java.ClassName
Expand All @@ -33,9 +33,10 @@ import org.checkerframework.checker.nullness.qual.Nullable
/**
* The [ClassName] of [org.checkerframework.checker.nullness.qual.Nullable].
*/
internal val NullableAnnotation = ClassName(Nullable::class)
public val NullableAnnotation: ClassName = ClassName(Nullable::class)

/**
* The [TypeNameClass] annotated with [NullableAnnotation].
*/
internal val NullableTypeNameClass = AnnotatedClassName(TypeNameClass, NullableAnnotation)
public val NullableTypeNameClass: AnnotatedClassName =
AnnotatedClassName(TypeNameClass, NullableAnnotation)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

@file:JvmName("ClassNames")

package io.spine.validation.java.expression
package io.spine.validation.api.expression

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
Expand All @@ -53,52 +53,52 @@ import java.util.stream.Collectors
/**
* The [ClassName] of [String].
*/
internal val StringClass = ClassName(String::class)
public val StringClass: ClassName = ClassName(String::class)

/**
* The [ClassName] of [Collectors].
*/
internal val CollectorsClass = ClassName(Collectors::class)
public val CollectorsClass: ClassName = ClassName(Collectors::class)

/**
* The [ClassName] of [TemplateString].
*/
internal val TemplateStringClass = ClassName(TemplateString::class)
public val TemplateStringClass: ClassName = ClassName(TemplateString::class)

/**
* The [ClassName] of [Pattern].
*/
internal val PatternClass = ClassName(Pattern::class)
public val PatternClass: ClassName = ClassName(Pattern::class)

/**
* The [ClassName] of [ImmutableList].
*/
internal val ImmutableListClass = ClassName(ImmutableList::class)
public val ImmutableListClass: ClassName = ClassName(ImmutableList::class)

/**
* The [ClassName] of [ImmutableSet].
*/
internal val ImmutableSetClass = ClassName(ImmutableSet::class)
public val ImmutableSetClass: ClassName = ClassName(ImmutableSet::class)

/**
* The [ClassName] of [LinkedHashMultiset].
*/
internal val LinkedHashMultisetClass = ClassName(LinkedHashMultiset::class)
public val LinkedHashMultisetClass: ClassName = ClassName(LinkedHashMultiset::class)

/**
* The [ClassName] of [Multiset.Entry] class.
*/
internal val MultiSetEntryClass = ClassName(Multiset.Entry::class)
public val MultiSetEntryClass: ClassName = ClassName(Multiset.Entry::class)

/**
* The [ClassName] of [FieldPath].
*/
internal val FieldPathClass = ClassName(FieldPath::class)
public val FieldPathClass: ClassName = ClassName(FieldPath::class)

/**
* The [ClassName] of [ConstraintViolation].
*/
internal val ConstraintViolationClass = ClassName(ConstraintViolation::class)
public val ConstraintViolationClass: ClassName = ClassName(ConstraintViolation::class)

/**
* The [ClassName] of `io.spine.base.FieldPaths`.
Expand All @@ -107,69 +107,69 @@ internal val ConstraintViolationClass = ClassName(ConstraintViolation::class)
* declared for [FieldPath]. It is available from Java, but not from Kotlin.
* So, we specify it as a string literal here.
*/
internal val FieldPathsClass = ClassName("io.spine.base", "FieldPaths")
public val FieldPathsClass: ClassName = ClassName("io.spine.base", "FieldPaths")

/**
* The [ClassName] of [ValidationError].
*/
internal val ValidationErrorClass = ClassName(ValidationError::class)
public val ValidationErrorClass: ClassName = ClassName(ValidationError::class)

/**
* The [ClassName] of [ValidatableMessage].
*/
internal val ValidatableMessageClass = ClassName(ValidatableMessage::class)
public val ValidatableMessageClass: ClassName = ClassName(ValidatableMessage::class)

/**
* The [ClassName] of [AnyPacker].
*/
internal val AnyPackerClass = ClassName(AnyPacker::class)
public val AnyPackerClass: ClassName = ClassName(AnyPacker::class)

/**
* The [ClassName] of [Message].
*/
internal val MessageClass = ClassName(Message::class)
public val MessageClass: ClassName = ClassName(Message::class)

/**
* The [ClassName] of Protobuf [Any].
*/
internal val AnyClass = ClassName(Any::class)
public val AnyClass: ClassName = ClassName(Any::class)

/**
* The [ClassName] of [KnownTypes].
*/
internal val KnownTypesClass = ClassName(KnownTypes::class)
public val KnownTypesClass: ClassName = ClassName(KnownTypes::class)

/**
* The [ClassName] of [TypeUrl].
*/
internal val TypeUrlClass = ClassName(TypeUrl::class)
public val TypeUrlClass: ClassName = ClassName(TypeUrl::class)

/**
* The [ClassName] of [TypeName] from `java-base`.
*/
internal val TypeNameClass = ClassName(TypeName::class)
public val TypeNameClass: ClassName = ClassName(TypeName::class)

/**
* The [ClassName] of [java.lang.Integer].
*/
internal val IntegerClass = ClassName(Integer::class)
public val IntegerClass: ClassName = ClassName(Integer::class)

/**
* The [ClassName] of [java.lang.Long].
*/
internal val LongClass = ClassName(java.lang.Long::class)
public val LongClass: ClassName = ClassName(java.lang.Long::class)

/**
* The [ClassName] of [java.util.Objects].
*/
internal val ObjectsClass = ClassName(Objects::class)
public val ObjectsClass: ClassName = ClassName(Objects::class)

/**
* The [ClassName] of [Timestamps].
*/
internal val TimestampsClass = ClassName(Timestamps::class)
public val TimestampsClass: ClassName = ClassName(Timestamps::class)

/**
* The [ClassName] of [io.spine.base.Time].
*/
internal val SpineTime = ClassName(Time::class)
public val SpineTime: ClassName = ClassName(Time::class)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

@file:JvmName("ConstraintViolations")

package io.spine.validation.java.violation
package io.spine.validation.api.expression

import io.spine.base.FieldPath
import io.spine.protobuf.restoreProtobufEscapes
Expand All @@ -39,9 +39,6 @@ import io.spine.protodata.java.packToAny
import io.spine.validate.ConstraintViolation
import io.spine.validate.TemplateString
import io.spine.validate.checkPlaceholdersHasValue
import io.spine.validation.ErrorPlaceholder
import io.spine.validation.java.expression.StringClass
import io.spine.validation.java.expression.TemplateStringClass

/**
* Yields an expression that creates a new instance of [ConstraintViolation]
Expand All @@ -58,7 +55,7 @@ import io.spine.validation.java.expression.TemplateStringClass
* the invalid field value for this option is the field type's default value,
* which is treated as "no value" at all.
*/
internal fun constraintViolation(
public fun constraintViolation(
errorMessage: Expression<TemplateString>,
typeName: Expression<String>,
fieldPath: Expression<FieldPath>?,
Expand All @@ -77,27 +74,25 @@ internal fun constraintViolation(
}

/**
* Yields an expression that creates a new instance of [TemplateString]
* with the given parameters.
* Yields an expression that creates a new instance of [TemplateString].
*
* @param template The template string that may have one or more placeholders.
* @param placeholders The supported placeholders and their values.
* @param optionName The name of the option, which declared the provided [placeholders].
*/
internal fun templateString(
public fun templateString(
template: String,
placeholders: Map<ErrorPlaceholder, Expression<String>>,
placeholders: Map<String, Expression<String>>,
optionName: String
): Expression<TemplateString> {
checkPlaceholdersHasValue(template, placeholders.mapKeys { it.key.value }) { missingKeys ->
checkPlaceholdersHasValue(template, placeholders) { missingKeys ->
"Unexpected error message placeholders `$missingKeys` specified for the `($optionName)`" +
" option. The available placeholders: `${placeholders.keys}`. Please make sure" +
" that the policy that verifies the message placeholders and its code generator" +
" operate with the same set of placeholders."
}
val placeholderEntries = mapExpression(
StringClass, StringClass,
placeholders.mapKeys { StringLiteral(it.key.toString()) }
placeholders.mapKeys { StringLiteral(it.key) }
)
val escapedTemplate = restoreProtobufEscapes(template)
return TemplateStringClass.newBuilder()
Expand Down
Loading
Loading