diff --git a/buildSrc/src/main/kotlin/JavaBasedProjectConventions.kt b/buildSrc/src/main/kotlin/JavaBasedProjectConventions.kt index bbfaecc1..48160c3e 100644 --- a/buildSrc/src/main/kotlin/JavaBasedProjectConventions.kt +++ b/buildSrc/src/main/kotlin/JavaBasedProjectConventions.kt @@ -24,6 +24,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun Project.javaBasedProjectConventions() { repositories { mavenCentral() + mavenLocal() } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e108428..9cabb21e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ protobuf-java = "4.30.1" protobuf-js = "7.4.0" protobufGradlePlugin = "0.9.5" protovalidate = "0.13.0" -protovalidateJava = "0.12.0" +protovalidateJava = "0.0.0-SNAPSHOT" slf4j = "2.0.17" # build diff --git a/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/PackageHacks.kt b/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/PackageHacks.kt index c0bfb680..96d1144b 100644 --- a/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/PackageHacks.kt +++ b/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/PackageHacks.kt @@ -43,9 +43,15 @@ internal class ProtoktEvaluatorBuilder( internal class ProtoktEvaluator( private val evaluator: Evaluator ) { - fun evaluate(message: Message, runtimeContext: RuntimeContext, failFast: Boolean) = - evaluator.evaluate(MessageValue(message.toDynamicMessage(runtimeContext)), failFast) - .let(::ProtoktRuleViolationBuilders) + fun evaluate(message: Message, runtimeContext: RuntimeContext, failFast: Boolean, lazyConvert: Boolean) = + evaluator.evaluate( + if (lazyConvert) { + ProtoktMessageValue(message, runtimeContext) + } else { + MessageValue(message.toDynamicMessage(runtimeContext)) + }, + failFast + ).let(::ProtoktRuleViolationBuilders) } internal class ProtoktRuleViolationBuilders( diff --git a/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/ProtoktImpl.kt b/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/ProtoktImpl.kt new file mode 100644 index 00000000..26724f55 --- /dev/null +++ b/protokt-protovalidate/src/main/kotlin/build/buf/protovalidate/ProtoktImpl.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 Toast, Inc. + * + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package build.buf.protovalidate + +import com.google.common.primitives.UnsignedLong +import com.google.protobuf.Descriptors.FieldDescriptor +import protokt.v1.Bytes +import protokt.v1.Enum +import protokt.v1.Message +import protokt.v1.google.protobuf.RuntimeContext +import protokt.v1.google.protobuf.getField +import protokt.v1.google.protobuf.hasField + +internal class ProtoktMessageLike( + val message: Message, + val context: RuntimeContext, +) : MessageReflector { + override fun hasField(field: FieldDescriptor) = + message.hasField(field) + + override fun getField(field: FieldDescriptor) = + ProtoktObjectValue( + field, + message.getField(field)!!, + context + ) +} + +internal class ProtoktMessageValue( + private val message: Message, + private val context: RuntimeContext, +) : Value { + override fun fieldDescriptor() = + null + + override fun messageValue() = + ProtoktMessageLike(message, context) + + override fun repeatedValue() = + emptyList() + + override fun mapValue() = + emptyMap() + + override fun celValue() = + context.convertValue(message) + + override fun jvmValue(clazz: Class) = + null +} + +internal class ProtoktObjectValue( + private val fieldDescriptor: FieldDescriptor, + private val value: Any, + private val context: RuntimeContext, +) : Value { + override fun fieldDescriptor() = + fieldDescriptor + + override fun messageValue(): MessageReflector = + ProtoktMessageLike(value as Message, context) + + override fun repeatedValue() = + (value as List<*>).map { ProtoktObjectValue(fieldDescriptor, it!!, context) } + + override fun mapValue(): Map { + val input = value as Map<*, *> + + val keyDesc = fieldDescriptor.messageType.findFieldByNumber(1) + val valDesc = fieldDescriptor.messageType.findFieldByNumber(2) + + return input.entries.associate { (key, value) -> + Pair( + ProtoktObjectValue(keyDesc, key!!, context), + ProtoktObjectValue(valDesc, value!!, context), + ) + } + } + + override fun celValue() = + when (value) { + is Enum -> value.value.toLong() + is Float -> value.toDouble() + is Int -> value.toLong() + is UInt -> UnsignedLong.valueOf(value.toLong()) + is ULong -> UnsignedLong.valueOf(value.toLong()) + is Message, is Bytes -> context.convertValue(value) + + // pray + else -> value + } + + override fun jvmValue(clazz: Class): T? = + context.convertValue(value) + ?.let { + when (it) { + is Int -> it.toLong() + is Float -> it.toDouble() + else -> it + } + } + ?.let(clazz::cast) +} diff --git a/protokt-protovalidate/src/main/kotlin/protokt/v1/buf/validate/Validator.kt b/protokt-protovalidate/src/main/kotlin/protokt/v1/buf/validate/Validator.kt index 84c1f661..206ee334 100644 --- a/protokt-protovalidate/src/main/kotlin/protokt/v1/buf/validate/Validator.kt +++ b/protokt-protovalidate/src/main/kotlin/protokt/v1/buf/validate/Validator.kt @@ -30,7 +30,8 @@ import kotlin.reflect.full.findAnnotation @Beta class Validator @JvmOverloads constructor( - config: Config = Config.newBuilder().build() + config: Config = Config.newBuilder().build(), + private val lazyConvert: Boolean = true ) { private val evaluatorBuilder = ProtoktEvaluatorBuilder(config) @@ -57,7 +58,7 @@ class Validator @JvmOverloads constructor( val result = evaluatorsByFullTypeName .getValue(message::class.findAnnotation()!!.fullTypeName) - .evaluate(message, runtimeContext, failFast) + .evaluate(message, runtimeContext, failFast, lazyConvert) return if (result.isEmpty()) { ValidationResult.EMPTY diff --git a/settings.gradle.kts b/settings.gradle.kts index 98a35fbf..ae0c2a52 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,6 +71,7 @@ include( "testing:protokt-generation-2", "testing:protobuf-java", "testing:protovalidate-conformance", + "testing:protovalidate-conformance:protos", "testing:protobufjs", "testing:testing-util", diff --git a/testing/protovalidate-conformance/build.gradle.kts b/testing/protovalidate-conformance/build.gradle.kts index d2707ca5..0630e732 100644 --- a/testing/protovalidate-conformance/build.gradle.kts +++ b/testing/protovalidate-conformance/build.gradle.kts @@ -13,8 +13,6 @@ * limitations under the License. */ -import com.google.protobuf.gradle.GenerateProtoTask -import com.google.protobuf.gradle.proto import org.gradle.api.distribution.plugins.DistributionPlugin.TASK_INSTALL_NAME plugins { @@ -22,11 +20,10 @@ plugins { application } -localProtokt(false) - dependencies { implementation(project(":protokt-protovalidate")) implementation(project(":protokt-reflect")) + implementation(project(":testing:protovalidate-conformance:protos")) implementation(kotlin("reflect")) implementation(libs.cel) implementation(libs.classgraph) @@ -36,39 +33,10 @@ dependencies { testImplementation(libs.truth) } -sourceSets.main { - proto { - srcDir(project.layout.buildDirectory.file("protovalidate/export")) - } -} - val protovalidateVersion = libs.versions.protovalidate.get() val gobin = project.layout.buildDirectory.file("gobin").get().asFile.absolutePath -val bufExecutable = project.layout.buildDirectory.file("gobin/buf").get().asFile val conformanceExecutable = project.layout.buildDirectory.file("gobin/protovalidate-conformance").get().asFile -val installBuf = - tasks.register("installBuf") { - environment("GOBIN", gobin) - outputs.file(bufExecutable) - commandLine("go", "install", "github.com/bufbuild/buf/cmd/buf@v${libs.versions.buf.get()}") - } - -val downloadConformanceProtos = - tasks.register("downloadConformanceProtos") { - dependsOn(installBuf) - commandLine( - bufExecutable, - "export", - "buf.build/bufbuild/protovalidate-testing:v$protovalidateVersion", - "--output=build/protovalidate/export" - ) - } - -tasks.withType { - dependsOn(downloadConformanceProtos) -} - val installConformance = tasks.register("installProtovalidateConformance") { environment("GOBIN", gobin) @@ -80,13 +48,16 @@ val installConformance = ) } +val lazyBufImpl: String by project + val conformance = tasks.register("conformance") { dependsOn(TASK_INSTALL_NAME, installConformance) description = "Runs protovalidate conformance tests." environment( "JAVA_OPTS" to "-Xmx64M", - "GOMEMLIMIT" to "40MiB" + "GOMEMLIMIT" to "40MiB", + "LAZY_BUF_IMPL" to lazyBufImpl ) commandLine( conformanceExecutable.absolutePath, diff --git a/testing/protovalidate-conformance/protos/build.gradle.kts b/testing/protovalidate-conformance/protos/build.gradle.kts new file mode 100644 index 00000000..08919b4c --- /dev/null +++ b/testing/protovalidate-conformance/protos/build.gradle.kts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 Toast, Inc. + * + * 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 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.protobuf.gradle.GenerateProtoTask +import com.google.protobuf.gradle.proto +import protokt.v1.gradle.ProtoktExtension + +plugins { + id("protokt.jvm-conventions") +} + +localProtokt(false) + +sourceSets.main { + proto { + srcDir(project.layout.buildDirectory.file("protovalidate/export")) + } +} + +configure { + generate { + // lots of protos; this would take a long time + formatOutput = false + } +} + +dependencies { + implementation(libs.protobuf.java) +} + +val protovalidateVersion = libs.versions.protovalidate.get() +val gobin = project.layout.buildDirectory.file("gobin").get().asFile.absolutePath +val bufExecutable = project.layout.buildDirectory.file("gobin/buf").get().asFile +val conformanceExecutable = project.layout.buildDirectory.file("gobin/protovalidate-conformance").get().asFile + +val installBuf = + tasks.register("installBuf") { + environment("GOBIN", gobin) + outputs.file(bufExecutable) + commandLine("go", "install", "github.com/bufbuild/buf/cmd/buf@v${libs.versions.buf.get()}") + } + +val downloadConformanceProtos = + tasks.register("downloadConformanceProtos") { + dependsOn(installBuf) + commandLine( + bufExecutable, + "export", + "buf.build/bufbuild/protovalidate-testing:v$protovalidateVersion", + "--output=build/protovalidate/export" + ) + } + +tasks.withType { + dependsOn(downloadConformanceProtos) +} diff --git a/testing/protovalidate-conformance/src/main/kotlin/protokt/v1/buf/validate/conformance/Main.kt b/testing/protovalidate-conformance/src/main/kotlin/protokt/v1/buf/validate/conformance/Main.kt index ceed1926..d98e75c1 100644 --- a/testing/protovalidate-conformance/src/main/kotlin/protokt/v1/buf/validate/conformance/Main.kt +++ b/testing/protovalidate-conformance/src/main/kotlin/protokt/v1/buf/validate/conformance/Main.kt @@ -47,7 +47,8 @@ object Main { Config.newBuilder() .setTypeRegistry(createTypeRegistry(fileDescriptors)) .setExtensionRegistry(createExtensionRegistry(fileDescriptors)) - .build() + .build(), + System.getenv("LAZY_BUF_IMPL").toBoolean() ) loadValidDescriptors(validator, descriptorMap.values) return TestConformanceResponse