Skip to content

Commit

Permalink
Implement (set_once) support for String fields
Browse files Browse the repository at this point in the history
  • Loading branch information
yevhenii-nadtochii committed Oct 21, 2024
1 parent 450f1ec commit 27cb959
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,17 @@

package io.spine.test.tools.validate;

import com.google.common.truth.Correspondence;
import com.google.protobuf.ByteString;
import io.spine.validate.ConstraintViolation;
import io.spine.validate.ValidationException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static com.google.common.truth.Correspondence.transforming;
import static com.google.protobuf.ByteString.copyFromUtf8;
import static org.junit.jupiter.api.Assertions.assertThrows;

@DisplayName("`(set_once)` constraint should be compiled so that")
class SetOnceConstraintTest {

private static final Correspondence<ConstraintViolation, String> fieldName = transforming(
violation -> violation.getFieldPath().getFieldName(0),
"field name"
);

@Nested
@DisplayName("when set, the field is prohibited for overriding")
class WhenSet {
Expand All @@ -53,7 +45,35 @@ class WhenSet {
@DisplayName("of a string value")
void stringValue() {
var student = Student.newBuilder()
.setIdBytes(ByteString.copyFromUtf8("student-id-1"))
.setId("student-id-1")
.build();
assertThrows(
ValidationException.class,
() -> student.toBuilder()
.setId("student-id-2")
.build()
);
}

@Test
@DisplayName("of a string bytes value")
void stringBytesValue() {
var student = Student.newBuilder()
.setIdBytes(copyFromUtf8("student-id-1"))
.build();
assertThrows(
ValidationException.class,
() -> student.toBuilder()
.setIdBytes(copyFromUtf8("student-id-2"))
.build()
);
}

@Test
@DisplayName("of a bytes string to string value")
void stringBytesToStringValue() {
var student = Student.newBuilder()
.setIdBytes(copyFromUtf8("student-id-1"))
.build();
assertThrows(
ValidationException.class,
Expand All @@ -62,5 +82,19 @@ void stringValue() {
.build()
);
}

@Test
@DisplayName("of a string value to string bytes")
void stringValueToStringBytes() {
var student = Student.newBuilder()
.setId("student-id-1")
.build();
assertThrows(
ValidationException.class,
() -> student.toBuilder()
.setIdBytes(copyFromUtf8("student-id-2"))
.build()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import "google/protobuf/empty.proto";

message Student {

// TODO:2024-10-20:yevhenii.nadtochii: Try field names with a "_".
string id = 1 [(set_once) = true];
int32 age = 2 [(set_once) = true];
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public List<Renderer<?>> renderers() {
result.addAll(base.renderers());
result.add(new PrintValidationInsertionPoints(),
new JavaValidationRenderer(),
new ImplementValidatingBuilder());
new ImplementValidatingBuilder(),
new SetOnceValidationRenderer());
return result.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.java

import com.intellij.psi.PsiClass
import com.intellij.psi.PsiJavaFile
import io.spine.protodata.ast.field
import io.spine.protodata.ast.isPrimitive
import io.spine.protodata.java.ClassName
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.render.SourceFileSet
import io.spine.string.camelCase
import io.spine.tools.psi.java.Environment.elementFactory
import io.spine.tools.psi.java.execute
import io.spine.tools.psi.java.method
import io.spine.validation.SetOnceField

internal class SetOnceValidationRenderer : JavaRenderer() {

override fun render(sources: SourceFileSet) {
// We receive `grpc` and `kotlin` output roots here. Now we do only `java`.
if (!sources.hasJavaRoot) {
return
}
val messages = findMessageTypes().associateBy { it.message.name }
val fields = select<SetOnceField>().all()
val fieldsToMessages = fields.associateWith {
messages[it.id.type] ?: error("Messages `${it.id.name}` not found.")
}
fieldsToMessages.forEach {
val message = it.value.message
val file = sources.javaFileOf(message)
val className = message.javaClassName(it.value.fileHeader!!)
val builderClass = ClassName(className.packageName, className.simpleNames + "Builder")
val psiFile = file.psi() as PsiJavaFile
val psiClass = psiFile.findClass(builderClass)
execute {
psiClass.render(it.key, it.value)
}
file.overwrite(psiFile.text)
}
}

private fun PsiClass.render(setOnce: SetOnceField, message: MessageWithFile) {
val fieldName = setOnce.id.name.value
val field = message.message.field(fieldName)
val fieldType = field.type
when {
fieldType.isPrimitive && fieldType.primitive.name == "TYPE_STRING" -> {
alertStringSetter(fieldName, field.number)
}

fieldType.isPrimitive && fieldType.primitive.name == "TYPE_INT32" -> {
alertNumberSetter()
}

else -> error("Unsupported `(set_once)` field type: `$fieldType`")
}
}

private fun PsiClass.alertStringSetter(fieldName: String, fieldNumber: Int) {
val preconditionCheck =
"""
if (!${fieldName}_.equals(getDescriptorForType().findFieldByNumber($fieldNumber).getDefaultValue())) {
throw new io.spine.validate.ValidationException(io.spine.validate.ConstraintViolation.getDefaultInstance());
}""".trimIndent()
val statement = elementFactory.createStatementFromText(preconditionCheck, null)
val setter = method("set${fieldName.camelCase()}").body!!
val bytesSetter = method("set${fieldName.camelCase()}Bytes").body!!
setter.addAfter(statement, setter.lBrace)
bytesSetter.addAfter(statement, bytesSetter.lBrace)
}

private fun alertNumberSetter() {
println("Alerting Number setter")
}
}

0 comments on commit 27cb959

Please sign in to comment.