Skip to content

Remove rules-related classes #211

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

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e4ff995
Remove rules-related Proto messages
yevhenii-nadtochii Apr 14, 2025
6710d07
Remove no longer needed Java files
yevhenii-nadtochii Apr 14, 2025
67e9c4d
Introduce `CurrencyMessage` and `CurrencyMessageDiscovered` events
yevhenii-nadtochii Apr 15, 2025
52ab50a
Introduce `CurrencyRenderer`
yevhenii-nadtochii Apr 15, 2025
7f537b1
Rename `CurrencyValidationPolicy` to `CurrencyPolicy`
yevhenii-nadtochii Apr 15, 2025
29c2115
Merge branch 'master' into remove-rules
yevhenii-nadtochii Apr 15, 2025
ef05a39
Take the latest ProtoData
yevhenii-nadtochii Apr 15, 2025
9c81d8c
Take the latest ProtoData
yevhenii-nadtochii Apr 16, 2025
4cb3092
Re-implement `CurrencyPolicy`
yevhenii-nadtochii Apr 16, 2025
ddb15c4
Re-implement `CurrencyTypeView`
yevhenii-nadtochii Apr 16, 2025
b293976
Merge branch 'master' into remove-rules
yevhenii-nadtochii Apr 23, 2025
83f6df7
Register `View` with a class instance
yevhenii-nadtochii Apr 23, 2025
99387b9
Update docs to `CurrencyPolicy`
yevhenii-nadtochii Apr 23, 2025
2a602bc
Make use of error placeholder
yevhenii-nadtochii Apr 23, 2025
f78b894
Implement `CurrencyGenerator`
yevhenii-nadtochii Apr 23, 2025
37b8c24
Implements `CurrencyRenderer`
yevhenii-nadtochii Apr 23, 2025
15ecc35
Merge branch 'codegen-for-require' into remove-rules
yevhenii-nadtochii Apr 23, 2025
179f60e
Restore `public` modifiers
yevhenii-nadtochii Apr 23, 2025
b4d4d6f
Fix bugs of the `CurrencyGenerator`
yevhenii-nadtochii Apr 24, 2025
7d45197
Branch `stringTemplate()` method
yevhenii-nadtochii Apr 24, 2025
326874d
Merge branch 'master' into remove-rules
yevhenii-nadtochii Apr 24, 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
2 changes: 2 additions & 0 deletions java-tests/extensions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import io.spine.dependency.build.Ksp
import io.spine.dependency.lib.AutoService
import io.spine.dependency.lib.AutoServiceKsp
import io.spine.dependency.local.McJava
import io.spine.dependency.local.ProtoData

buildscript {
forceCodegenPlugins()
Expand All @@ -42,6 +43,7 @@ dependencies {
ksp(AutoServiceKsp.processor)

implementation(project(":java"))
implementation(ProtoData.java)
}

configurations.all {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.test

import io.spine.protodata.ast.TypeName
import io.spine.protodata.java.CodeBlock
import io.spine.protodata.java.Expression
import io.spine.protodata.java.ReadVar
import io.spine.protodata.java.field
import io.spine.server.query.Querying
import io.spine.server.query.select
import io.spine.validate.ConstraintViolation
import io.spine.validation.java.expression.orElse
import io.spine.validation.java.expression.stringify
import io.spine.validation.java.generate.OptionGenerator
import io.spine.validation.java.generate.SingleOptionCode
import io.spine.validation.java.generate.ValidationCodeInjector.MessageScope.message
import io.spine.validation.java.generate.ValidationCodeInjector.ValidateScope.parentName
import io.spine.validation.java.generate.ValidationCodeInjector.ValidateScope.violations
import io.spine.validation.java.violation.constraintViolation
import io.spine.validation.java.violation.templateString
import io.spine.validation.test.money.CurrencyMessage

/**
* The generator for the `(currency)` option.
*/
internal class CurrencyGenerator(private val querying: Querying) : OptionGenerator {

/**
* All `(currency)`-marked messages in the current compilation process.
*/
private val allCurrencyMessages by lazy {
querying.select<CurrencyMessage>()
.all()
}

override fun codeFor(type: TypeName): List<SingleOptionCode> {
val requireMessage = allCurrencyMessages.find { it.type == type }
if (requireMessage == null) {
return emptyList()
}
val code = GenerateCurrency(requireMessage).code()
return listOf(code)
}
}

/**
* Generates code for a single application of the `(currency)` option
* represented by the [view].
*/
private class GenerateCurrency(private val view: CurrencyMessage) {

private val minorField = view.minorUnitField
private val minorThreshold = view.currency.minorUnits

/**
* Returns the generated code.
*/
fun code(): SingleOptionCode {
val getter = message.field(minorField)
.getter<Int>()
val constraint = CodeBlock(
"""
if ($getter > $minorThreshold) {
var typeName = ${parentName.orElse(view.type)};
var violation = ${violation(ReadVar("typeName"), getter)};
$violations.add(violation);
}
""".trimIndent()
)
return SingleOptionCode(constraint)
}

private fun violation(
typeName: Expression<TypeName>,
minorValue: Expression<Int>
): Expression<ConstraintViolation> {
val typeNameStr = typeName.stringify()
val placeholders = supportedPlaceholders(minorValue)
val errorMessage = templateString(view.errorMessage, placeholders, CURRENCY)
return constraintViolation(errorMessage, typeNameStr, fieldPath = null, fieldValue = null)
}

private fun supportedPlaceholders(
minorValue: Expression<Int>
): Map<String, Expression<String>> = mapOf("minor.value" to minorValue.stringify(),)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2024, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.test

import io.spine.core.External
import io.spine.core.Where
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.PrimitiveType.TYPE_INT32
import io.spine.protodata.ast.PrimitiveType.TYPE_INT64
import io.spine.protodata.ast.event.MessageOptionDiscovered
import io.spine.protodata.ast.unpack
import io.spine.protodata.check
import io.spine.protodata.plugin.Policy
import io.spine.server.event.Just
import io.spine.server.event.React
import io.spine.server.event.just
import io.spine.validation.OPTION_NAME
import io.spine.validation.test.money.Currency
import io.spine.validation.test.money.CurrencyMessageDiscovered
import io.spine.validation.test.money.currencyMessageDiscovered

/**
* The name of the option.
*/
internal const val CURRENCY = "currency"

/**
* Controls whether a message should be validated with the `(currency)` option.
*
* Whenever a message marked with the `(currency)` option is discovered, emits
* [CurrencyMessageDiscovered] event if the message has exactly two integer fields.
*
* Otherwise, a compilation error is reported.
*/
public class CurrencyPolicy : Policy<MessageOptionDiscovered>() {

@React
override fun whenever(
@External @Where(field = OPTION_NAME, equals = CURRENCY)
event: MessageOptionDiscovered
): Just<CurrencyMessageDiscovered> {
val file = event.file
val messageType = event.subject
val fields = messageType.fieldList
checkFieldType(fields.size == 2, file, messageType)

val firstField = messageType.fieldList[0]
val secondField = messageType.fieldList[1]
checkFieldType(firstField.isInteger && secondField.isInteger, file, messageType)

val option = event.option.unpack<Currency>()
val message = errorMessage(firstField, secondField, option.minorUnits)
return currencyMessageDiscovered {
type = messageType.name
currency = option
majorUnitField = firstField
minorUnitField = secondField
errorMessage = message
}.just()
}
}

private val Field.isInteger: Boolean
get() = type.primitive in listOf(TYPE_INT32, TYPE_INT64)

private fun errorMessage(minor: Field, major: Field, minorUnits: Int) =
"Expected `${minor.name.value}` field to have less than `$minorUnits`" +
" per one unit in `${major.name.value}` field. The passed value: `\${minor.value}`."

private fun checkFieldType(condition: Boolean, file: File, message: MessageType) =
Compilation.check(condition, file, message.span) {
"The `($CURRENCY)` option cannot be applied to `${message.qualifiedName}`. It is" +
" applicable only to messages that have exactly two integer fields."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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.test

import com.intellij.psi.PsiJavaFile
import io.spine.protodata.java.file.hasJavaRoot
import io.spine.protodata.java.javaClassName
import io.spine.protodata.java.render.JavaRenderer
import io.spine.protodata.java.render.findClass
import io.spine.protodata.java.render.findMessageTypes
import io.spine.protodata.render.SourceFile
import io.spine.protodata.render.SourceFileSet
import io.spine.tools.code.Java
import io.spine.validation.java.generate.MessageValidationCode
import io.spine.validation.java.generate.ValidationCodeInjector

/**
* Renders Java code for the `(currency)` option.
*/
public class CurrencyRenderer : JavaRenderer() {

private val codeInjector = ValidationCodeInjector()
private val querying = this@CurrencyRenderer
private val currencyGenerator = CurrencyGenerator(querying)

override fun render(sources: SourceFileSet) {
// We receive `grpc` and `kotlin` output sources roots here as well.
// As for now, we modify only `java` sources.
if (!sources.hasJavaRoot) {
return
}

findMessageTypes()
.mapNotNull { message ->
val optionCode = currencyGenerator.codeFor(message.name)
.firstOrNull()
optionCode?.let {
message to it
}
}
.forEach { (message, optionCode) ->
val messageCode = MessageValidationCode(
message = message.name.javaClassName(typeSystem),
constraints = listOf(optionCode.constraint),
fields = emptyList(),
methods = emptyList(),
)
val file = sources.javaFileOf(message)
file.render(messageCode)
}
}

private fun SourceFile<Java>.render(code: MessageValidationCode) {
val psiFile = psi() as PsiJavaFile
val messageClass = psiFile.findClass(code.message)
codeInjector.inject(code, messageClass)
overwrite(psiFile.text)
}
}
Loading
Loading