Skip to content
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

support protovalidate validation #259

Open
wants to merge 48 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6920b0e
set up protovalidate module
andrewparmet May 12, 2024
4ba0950
remove unneeded code
andrewparmet May 12, 2024
c4f78aa
just use go
andrewparmet May 12, 2024
df2e8db
extract buf version
andrewparmet May 12, 2024
0cd3157
enable lazy conversion
andrewparmet May 12, 2024
c5f0069
Revert "enable lazy conversion"
andrewparmet May 12, 2024
a9acd63
restrict accepted packages
andrewparmet May 12, 2024
698609b
close resource
andrewparmet May 12, 2024
97717ba
oops
andrewparmet May 12, 2024
94b96a3
try debug
andrewparmet May 12, 2024
3cbe1b5
really
andrewparmet May 12, 2024
be07932
log more
andrewparmet May 12, 2024
99587fa
stop ignoring error
andrewparmet May 12, 2024
3304d1f
oof
andrewparmet May 12, 2024
bfa5fa2
try again
andrewparmet May 13, 2024
1b6a44d
more refinement
andrewparmet May 13, 2024
fe3665d
imports
andrewparmet May 13, 2024
8a01142
last removal
andrewparmet May 13, 2024
632cdd6
overloads
andrewparmet May 13, 2024
37fc289
remove unneeded iteration
andrewparmet May 13, 2024
8b9c44a
begin simplifying
andrewparmet May 13, 2024
6653e75
simplify
andrewparmet May 13, 2024
2b78bba
apidump
andrewparmet May 13, 2024
048c965
fix
andrewparmet May 13, 2024
42d1ef6
apidump
andrewparmet May 13, 2024
bc3e7fb
package change
andrewparmet May 13, 2024
be7b6f3
Merge branch 'main' into protokt-protovalidate
andrewparmet May 15, 2024
7604111
try using junit to run the runner
andrewparmet May 18, 2024
1faf174
try setting java opts
andrewparmet May 18, 2024
7d72f62
publish test results
andrewparmet May 18, 2024
abb17e5
always
andrewparmet May 18, 2024
0394c45
debugging changes
andrewparmet May 18, 2024
5da6b1c
log failure
andrewparmet May 18, 2024
9d17b30
restrict driver
andrewparmet May 19, 2024
c51c054
print stack trace
andrewparmet May 19, 2024
b222673
improve conformance enforcement
andrewparmet May 19, 2024
a125e7e
severely restrict jvm memory
andrewparmet May 28, 2024
9d69198
run directly
andrewparmet May 28, 2024
f7bb177
Merge branch 'main' into protokt-protovalidate
andrewparmet May 28, 2024
e61a0df
lint
andrewparmet May 28, 2024
4cd8b94
try restricting go runtime
andrewparmet May 30, 2024
60302b4
bump mem
andrewparmet May 30, 2024
57e4640
cleanup
andrewparmet May 30, 2024
824775f
lint
andrewparmet May 30, 2024
a7b3f56
rename
andrewparmet May 30, 2024
ce4e1cd
meh, don't be mutable
andrewparmet May 30, 2024
1421ef4
make users synchronize
andrewparmet May 30, 2024
37f9638
meh, make it thread safe
andrewparmet Jun 12, 2024
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
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

[versions]
autoService = "1.1.1"
cel = "0.4.4"
grpc-java = "1.64.0"
grpc-kotlin = "1.4.1"
kotlinLogging = "5.1.0"
Expand All @@ -23,6 +24,8 @@ ktlint = "1.2.1"
protobuf-java = "3.21.7"
protobuf-js = "7.2.6"
protobufGradlePlugin = "0.9.4"
protovalidate = "0.6.4"
protovalidateJava = "0.2.1"
slf4j = "2.0.13"

# build
Expand All @@ -44,6 +47,7 @@ jmh = "1.37"
wire = "4.9.9"

# test
buf = "1.31.0"
classgraph = "4.8.153"
grpc-js = "1.8.14"
jackson = "2.17.1"
Expand All @@ -62,6 +66,7 @@ wire = { id = "com.squareup.wire", version.ref = "wire" }
[libraries]
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
autoServiceAnnotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
cel = { module = "org.projectnessie.cel:cel-tools", version.ref = "cel" }
grpc-kotlin-gen = { module = "io.grpc:protoc-gen-grpc-kotlin", version.ref = "grpc-kotlin" }
grpc-netty = { module = "io.grpc:grpc-netty", version.ref = "grpc-java" }
grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc-java" }
Expand All @@ -73,6 +78,7 @@ ktlintRuleSetStandard = { module = "com.pinterest.ktlint:ktlint-ruleset-standard
protobuf-gradlePlugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobufGradlePlugin" }
protobuf-java = { module ="com.google.protobuf:protobuf-java", version.ref = "protobuf-java" }
protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf-java" }
protovalidateJava = { module = "build.buf:protovalidate", version.ref = "protovalidateJava" }
slf4jSimple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }

# build
Expand Down
8 changes: 8 additions & 0 deletions protokt-protovalidate/api/protokt-protovalidate.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public final class protokt/v1/buf/validate/Validator {
public fun <init> ()V
public fun <init> (Lbuild/buf/protovalidate/Config;)V
public synthetic fun <init> (Lbuild/buf/protovalidate/Config;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun load (Lcom/google/protobuf/Descriptors$Descriptor;)V
public final fun validate (Lprotokt/v1/Message;)Lbuild/buf/protovalidate/ValidationResult;
}

28 changes: 28 additions & 0 deletions protokt-protovalidate/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2024 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.
*/

plugins {
id("protokt.jvm-conventions")
}

enablePublishing()
trackKotlinApiCompatibility()

dependencies {
implementation(project(":protokt-reflect"))
implementation(kotlin("reflect"))
implementation(libs.cel)
implementation(libs.protovalidateJava)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2024 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 protokt.v1.buf.validate

import build.buf.protovalidate.Config
import build.buf.protovalidate.ValidationResult
import build.buf.protovalidate.internal.celext.ValidateLibrary
import build.buf.protovalidate.internal.evaluator.Evaluator
import build.buf.protovalidate.internal.evaluator.EvaluatorBuilder
import build.buf.protovalidate.internal.evaluator.MessageValue
import com.google.protobuf.Descriptors.Descriptor
import org.projectnessie.cel.Env
import org.projectnessie.cel.Library
import protokt.v1.Beta
import protokt.v1.GeneratedMessage
import protokt.v1.Message
import protokt.v1.google.protobuf.RuntimeContext
import protokt.v1.google.protobuf.toDynamicMessage
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.full.findAnnotation

@Beta
class Validator @JvmOverloads constructor(
config: Config = Config.newBuilder().build()
) {
private val evaluatorBuilder =
EvaluatorBuilder(
Env.newEnv(Library.Lib(ValidateLibrary())),
config.isDisableLazy
)

private val failFast = config.isFailFast

private val evaluatorsByFullTypeName = ConcurrentHashMap<String, Evaluator>()
private val descriptors = Collections.newSetFromMap(ConcurrentHashMap<Descriptor, Boolean>())

@Volatile
private var runtimeContext = RuntimeContext(emptyList())

fun load(descriptor: Descriptor) {
doLoad(descriptor)
runtimeContext = RuntimeContext(descriptors)
}

private fun doLoad(descriptor: Descriptor) {
descriptors.add(descriptor)
evaluatorsByFullTypeName[descriptor.fullName] = evaluatorBuilder.load(descriptor)
descriptor.nestedTypes.forEach(::doLoad)
}

fun validate(message: Message): ValidationResult =
evaluatorsByFullTypeName
.getValue(message::class.findAnnotation<GeneratedMessage>()!!.fullTypeName)
.evaluate(MessageValue(message.toDynamicMessage(runtimeContext)), failFast)
}
4 changes: 3 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ include(
"protokt-codegen",
"protokt-core",
"protokt-core-lite",
"protokt-gradle-plugin",
"protokt-protovalidate",
"protokt-reflect",
"protokt-runtime",
"protokt-runtime-grpc",
"protokt-runtime-grpc-lite",
"protokt-gradle-plugin",

"grpc-kotlin-shim",

Expand Down Expand Up @@ -70,6 +71,7 @@ include(
"testing:protokt-generation",
"testing:protokt-generation-2",
"testing:protobuf-java",
"testing:protovalidate-conformance",
"testing:protobufjs",
"testing:testing-util",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

package protokt.v1.conformance

import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import protokt.v1.testing.ProcessOutput
import protokt.v1.testing.projectRoot
import protokt.v1.testing.runCommand
import java.io.File
Expand Down Expand Up @@ -63,9 +63,15 @@ class ConformanceTest {
@EnumSource
fun `run conformance tests`(runner: ConformanceRunner) {
try {
command(runner)
.runCommand(projectRoot.toPath())
.orFail("Conformance tests failed", ProcessOutput.Src.ERR)
val output = command(runner).runCommand(projectRoot.toPath())
println(output.stderr)

assertThat(output.stderr).contains("CONFORMANCE SUITE PASSED")
val matches = " (\\d+) unexpected failures".toRegex().findAll(output.stderr).toList()
// the current implementation runs two conformance suites
assertThat(matches).hasSize(2)
matches.forEach { assertThat(it.groupValues[1].toInt()).isEqualTo(0) }
assertThat(output.exitCode).isEqualTo(0)
} catch (t: Throwable) {
if (failingTests.exists()) {
println("Failing tests:\n" + failingTests.readText())
Expand Down
93 changes: 93 additions & 0 deletions testing/protovalidate-conformance/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2024 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 org.gradle.api.distribution.plugins.DistributionPlugin.TASK_INSTALL_NAME

plugins {
id("protokt.jvm-conventions")
application
}

localProtokt(false)

dependencies {
implementation(project(":protokt-protovalidate"))
implementation(project(":protokt-reflect"))
implementation(kotlin("reflect"))
implementation(libs.cel)
implementation(libs.classgraph)
implementation(libs.protovalidateJava)

testImplementation(project(":testing:testing-util"))
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<Exec>("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<Exec>("downloadConformanceProtos") {
dependsOn(installBuf)
commandLine(
bufExecutable,
"export",
"buf.build/bufbuild/protovalidate-testing:v$protovalidateVersion",
"--output=build/protovalidate/export"
)
}

tasks.withType<GenerateProtoTask> {
dependsOn(downloadConformanceProtos)
}

val installConformance =
tasks.register<Exec>("installProtovalidateConformance") {
environment("GOBIN", gobin)
outputs.file(conformanceExecutable)
commandLine(
"go",
"install",
"github.com/bufbuild/protovalidate/tools/protovalidate-conformance@v$protovalidateVersion"
)
}

application {
mainClass.set("protokt.v1.buf.validate.conformance.Main")
}

tasks {
test {
systemProperty("conformance-runner", conformanceExecutable.absolutePath)
outputs.upToDateWhen { false }
dependsOn(installConformance, TASK_INSTALL_NAME)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 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 protokt.v1.buf.validate.conformance

import com.google.protobuf.ByteString
import io.github.classgraph.ClassGraph
import protokt.v1.Deserializer
import protokt.v1.GeneratedMessage
import protokt.v1.google.protobuf.Empty
import kotlin.reflect.full.findAnnotation

object DynamicConcreteMessageDeserializer {
private val deserializersByFullTypeName: Map<String, Deserializer<*>> by lazy {
ClassGraph()
.enableAnnotationInfo()
.acceptPackages(
"protokt.v1.buf.validate.conformance.*",
"protokt.v1.google.protobuf"
)
.scan()
.use { result ->
result.getClassesWithAnnotation(GeneratedMessage::class.java)
.map { it.loadClass().kotlin }
}
.associate { messageClass ->
messageClass.findAnnotation<GeneratedMessage>()!!.fullTypeName to
messageClass
.nestedClasses
.single { it.simpleName == Empty.Deserializer::class.simpleName }
.objectInstance as Deserializer<*>
}
}

fun parse(fullTypeName: String, bytes: ByteString) =
deserializersByFullTypeName.getValue(fullTypeName).deserialize(bytes.newInput())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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 protokt.v1.buf.validate.conformance

import com.google.protobuf.DescriptorProtos.FileDescriptorSet
import com.google.protobuf.Descriptors.Descriptor
import com.google.protobuf.Descriptors.FileDescriptor

fun parse(fileDescriptorSet: FileDescriptorSet): Map<String, Descriptor> =
parseFileDescriptors(fileDescriptorSet)
.values
.flatMap { fileDescriptor ->
fileDescriptor.messageTypes.map { messageType ->
messageType.fullName to messageType
}
}
.toMap()

private fun parseFileDescriptors(fileDescriptorSet: FileDescriptorSet): Map<String, FileDescriptor> =
fileDescriptorSet.fileList.fold(mutableMapOf()) { map, fileDescriptorProto ->
map[fileDescriptorProto.getName()] =
FileDescriptor.buildFrom(
fileDescriptorProto,
fileDescriptorProto.dependencyList.mapNotNull(map::get).toTypedArray(),
false
)
map
}
Loading