├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .mergify.yml ├── .scala-steward.conf ├── .scalafmt.conf ├── LICENSE ├── NOTICE ├── README.md ├── build.sbt ├── cats └── src │ └── main │ └── scala │ └── scalapb │ └── validate │ └── cats │ ├── CatsAdapters.scala │ ├── NonEmptyChainAdapter.scala │ ├── NonEmptySeqAdapter.scala │ └── NonEmptyVectorAdapter.scala ├── code-gen └── src │ └── main │ ├── scala-2.12 │ └── scalapb │ │ └── validate │ │ └── compat.scala │ ├── scala-2.13 │ └── scalapb │ │ └── validate │ │ └── compat.scala │ ├── scala-3 │ └── scalapb │ │ └── validate │ │ └── compat.scala │ └── scala │ └── scalapb │ └── validate │ ├── compiler │ ├── BooleanRulesGen.scala │ ├── BytesRuleGen.scala │ ├── CodeGenerator.scala │ ├── ComparativeRulesGen.scala │ ├── EnumRulesGen.scala │ ├── IgnoreEmptyRulesGen.scala │ ├── MapRulesGen.scala │ ├── MembershipRulesGen.scala │ ├── RepeatedRulesGen.scala │ ├── RequiredRulesGen.scala │ ├── Rule.scala │ ├── RulesGen.scala │ ├── Show.scala │ ├── StringRulesGen.scala │ ├── TimestampRulesGen.scala │ └── ValidatePreprocessor.scala │ ├── gen.scala │ └── preprocessor.scala ├── core └── src │ ├── main │ ├── protobuf │ │ └── scalapb │ │ │ ├── validate-options.proto │ │ │ └── validate.proto │ └── scala │ │ └── scalapb │ │ └── validate │ │ ├── ComparativeValidation.scala │ │ ├── EnumValidation.scala │ │ ├── MapValidation.scala │ │ ├── MembershipValidation.scala │ │ ├── RepeatedValidation.scala │ │ ├── RequiredValidation.scala │ │ ├── SetAdapter.scala │ │ ├── TimestampValidation.scala │ │ ├── Validator.scala │ │ └── WellKnownRegex.scala │ └── test │ └── scala │ └── scalapb │ └── validate │ └── ResultSpec.scala ├── e2e └── src │ ├── main │ ├── protobuf-scala2 │ │ └── transforms │ │ │ └── refined │ │ │ └── refined.proto │ ├── protobuf │ │ ├── cats │ │ │ ├── excluded.proto │ │ │ ├── non_empty_chain.proto │ │ │ ├── non_empty_seq.proto │ │ │ ├── non_empty_vector.proto │ │ │ ├── novalidate.proto │ │ │ ├── options.proto │ │ │ └── types.proto │ │ ├── example.proto │ │ ├── oneofs.proto │ │ ├── optional.proto │ │ ├── options.proto │ │ ├── required.proto │ │ ├── skip │ │ │ ├── ours.proto │ │ │ ├── third_party.proto │ │ │ └── third_party_options.proto │ │ ├── tests │ │ │ ├── generation │ │ │ │ └── multi_file_java_test │ │ │ │ │ ├── BUILD │ │ │ │ │ ├── README.md │ │ │ │ │ ├── main.cc │ │ │ │ │ └── test.proto │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── harness │ │ │ │ ├── BUILD │ │ │ │ ├── cases │ │ │ │ ├── .gitignore │ │ │ │ ├── BUILD │ │ │ │ ├── bool.proto │ │ │ │ ├── bytes.proto │ │ │ │ ├── enums.proto │ │ │ │ ├── filename-with-dash.proto │ │ │ │ ├── kitchen_sink.proto │ │ │ │ ├── maps.proto │ │ │ │ ├── messages.proto │ │ │ │ ├── numbers.proto │ │ │ │ ├── oneofs.proto │ │ │ │ ├── other_package │ │ │ │ │ ├── BUILD │ │ │ │ │ └── embed.proto │ │ │ │ ├── repeated.proto │ │ │ │ ├── strings.proto │ │ │ │ ├── subdirectory │ │ │ │ │ └── in_subdirectory.proto │ │ │ │ ├── wkt_any.proto │ │ │ │ ├── wkt_duration.proto │ │ │ │ ├── wkt_nested.proto │ │ │ │ ├── wkt_timestamp.proto │ │ │ │ ├── wkt_wrappers.proto │ │ │ │ └── yet_another_package │ │ │ │ │ ├── BUILD │ │ │ │ │ └── embed.proto │ │ │ │ ├── cc │ │ │ │ ├── BUILD │ │ │ │ ├── diamond_lib.cc │ │ │ │ ├── diamond_test.cc │ │ │ │ └── harness.cc │ │ │ │ ├── executor │ │ │ │ ├── BUILD │ │ │ │ ├── cases.go │ │ │ │ ├── executor.go │ │ │ │ ├── executor_test.sh │ │ │ │ ├── harness.go │ │ │ │ └── worker.go │ │ │ │ ├── go │ │ │ │ └── main │ │ │ │ │ ├── BUILD │ │ │ │ │ └── harness.go │ │ │ │ ├── harness.proto │ │ │ │ ├── java │ │ │ │ └── BUILD │ │ │ │ └── python │ │ │ │ ├── BUILD │ │ │ │ ├── harness.py │ │ │ │ └── requirements_test.py │ │ └── transforms │ │ │ ├── field.proto │ │ │ ├── options.proto │ │ │ ├── order.proto │ │ │ ├── order2.proto │ │ │ ├── order3.proto │ │ │ └── required.proto │ ├── scala-2 │ │ └── scalapb │ │ │ └── transforms │ │ │ └── refined │ │ │ └── package.scala │ └── scala │ │ ├── e2e │ │ └── cats │ │ │ └── alltypes │ │ │ └── package.scala │ │ └── scalapb │ │ ├── package.scala │ │ └── transforms │ │ ├── MyCustomType.scala │ │ └── PositiveInt.scala │ └── test │ ├── scala-2 │ └── scalapb │ │ └── validate │ │ └── transforms │ │ └── refined │ │ └── RefinedSpec.scala │ │ └── RefinedSpec.scala │ └── scala │ └── scalapb │ └── validate │ ├── OptionsSpec.scala │ ├── ScalaHarness.scala │ ├── SkipSpec.scala │ ├── ValidationHelpers.scala │ ├── ValidatorSpec.scala │ ├── cats │ ├── CatsTypesSpec.scala │ ├── ExcludedSpec.scala │ └── NonEmptySpec.scala │ └── transforms │ ├── OrderSpec.scala │ ├── RequiredSpec.scala │ └── TransformSpec.scala ├── example ├── build.sbt ├── project │ ├── build.properties │ └── plugins.sbt └── src │ └── main │ ├── protobuf │ └── proto3.proto │ └── scala │ └── Main.scala ├── get_bazelisk.sh ├── make_harness.sh ├── project ├── Settings.scala ├── TestProtosGenerator.scala ├── build.properties └── plugins.sbt ├── shell.nix └── sync_pgv_tests.sh /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.8.6 2 | 8fcae17ccd45b4ce4020f5c7dc626ace7893af21 3 | 4 | # Scala Steward: Reformat with scalafmt 3.9.7 5 | 38aee09b4d45aaa1911897db989acae1dcbd72a4 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: ["ubuntu-latest"] 10 | scala: [JVM2_12, JVM2_13, JVM3] 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Download test executor 17 | run: | 18 | ./make_harness.sh 19 | 20 | - uses: actions/setup-java@v4 21 | with: 22 | java-version: 11 23 | distribution: temurin 24 | 25 | - uses: sbt/setup-sbt@v1 26 | 27 | - name: Compile and test 28 | run: | 29 | sbt "core${{matrix.scala}}/test" "codeGen${{matrix.scala}}/test" "e2e${{matrix.scala}}/test" "e2e${{matrix.scala}}/Test/runMain scalapb.validate.ScalaHarness" 30 | 31 | - name: Formatting 32 | run: | 33 | sbt scalafmtCheckAll scalafmtSbtCheck 34 | 35 | - name: Run example project 36 | run: | 37 | cd example 38 | sbt run 39 | # Single final job for mergify. 40 | ci-passed: 41 | runs-on: ubuntu-latest 42 | needs: build 43 | steps: 44 | - run: ':' 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [master] 5 | tags: ["*"] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up JDK 8 12 | uses: actions/setup-java@v4 13 | with: 14 | java-version: 8 15 | distribution: temurin 16 | - uses: olafurpg/setup-gpg@v3 17 | - uses: sbt/setup-sbt@v1 18 | - name: Publish ${{ github.ref }} 19 | run: sbt ci-release 20 | env: 21 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 22 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 23 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 24 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | target 4 | .idea 5 | .metals 6 | .bloop 7 | metals.sbt 8 | examples/project/metals.sbt 9 | .vscode 10 | .pgv 11 | bazelisk 12 | .bsp 13 | executor.exe 14 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | - check-success=ci-passed 5 | 6 | pull_request_rules: 7 | - name: assign and label scala-steward's PRs 8 | conditions: 9 | - author=scala-steward 10 | actions: 11 | assign: 12 | users: [thesamet] 13 | label: 14 | add: [dependency-update] 15 | - name: merge scala-steward's PRs 16 | conditions: 17 | - author=scala-steward 18 | - check-success=ci-passed 19 | actions: 20 | queue: 21 | method: squash 22 | name: default 23 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | buildRoots = [ 2 | "." 3 | "example" 4 | ] 5 | 6 | updates.pin = [ 7 | { 8 | groupId = "org.scala-lang" 9 | artifactId = "scala3-library" 10 | version = "3.3." 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.7" 2 | runner.dialect = scala213source3 3 | rewrite.rules = [SortImports, RedundantBraces] 4 | fileOverride { 5 | "glob:**/core/target/jvm-3/src_managed/main/**" { 6 | runner.dialect = scala3 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | scalapb-validate 2 | Copyright 2020, Nadav Samet and ScalaPB Project Authors 3 | 4 | Licensed under Apache License 2.0. See LICENSE for terms. 5 | 6 | This project bundles the following dependencies under the Apache Software 7 | License 2.0: 8 | 9 | * protoc-gen-validate 10 | Copyright 2019 Envoy Project Authors 11 | Licensed under Apache License 2.0. See LICENSE for terms. 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Snapshot Artifacts][Badge-SonatypeSnapshots]][Link-SonatypeSnapshots] 2 | 3 | ## Running the test harness (Linux only) 4 | 5 | PGV repo includes a comprehensive [test suite](https://github.com/envoyproxy/protoc-gen-validate/blob/master/tests/harness/executor/cases.go) defined over these [proto files](https://github.com/envoyproxy/protoc-gen-validate/tree/master/tests/harness/cases). 6 | 7 | To run this test suite against scalapb-validate: 8 | 9 | 1. Run `./make_harness.sh`. This will download a prebuilt statically-compiled executor built from protoc-gen-validate that runs the test harness. 10 | 11 | 2. In SBT, run `e2eJVM2_13/test:runMain scalapb.validate.ScalaHarness`. 12 | 13 | ## Adding the latest snapshot release to your project 14 | 15 | 1. Note the latest snapshot version on [Sonatype Snapshots](https://oss.sonatype.org/content/repositories/snapshots/com/thesamet/scalapb/scalapb-validate-core_2.13/) 16 | 17 | [![Snapshot Artifacts][Badge-SonatypeSnapshots]][Link-SonatypeSnapshots] 18 | 19 | 1. Add the following to your `project/plugins.sbt`, replace the 20 | validateVersion value with the snapshot version you have found in the 21 | previous step: 22 | ``` 23 | ThisBuild / resolvers += Resolver.sonatypeRepo("snapshots") 24 | 25 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.34") 26 | 27 | libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.10.8" 28 | 29 | val validateVersion = "0.1.2" 30 | 31 | libraryDependencies += "com.thesamet.scalapb" %% "scalapb-validate-codegen" % validateVersion 32 | ``` 33 | 34 | 1. Add the following to your `build.sbt`: 35 | ``` 36 | ThisBuild / resolvers += Resolver.sonatypeRepo("snapshots") 37 | 38 | Compile / PB.targets := Seq( 39 | scalapb.gen() -> (Compile / sourceManaged).value / "scalapb", 40 | scalapb.validate.gen() -> (Compile / sourceManaged).value / "scalapb" 41 | ) 42 | 43 | libraryDependencies ++= Seq( 44 | "com.thesamet.scalapb" %% "scalapb-validate-core" % scalapb.validate.compiler.BuildInfo.version % "protobuf" 45 | ) 46 | ``` 47 | 48 | 1. import `validate/validate.proto` in your protobufs and set up validators as described in [protoc-gen-validate documentation](https://github.com/envoyproxy/protoc-gen-validate). 49 | 50 | 1. The generated code will generate a Validator object for each message class. For example, if you have a `Person` message, it will generate a `PersonValidator` object that has a `validate(instance: Person)` method that returns a validation [`Result`](https://github.com/scalapb/scalapb-validate/blob/master/core/src/main/scala/scalapb/validate/Validator.scala). 51 | 52 | ## Examples 53 | See a full example at the [examples directory](https://github.com/scalapb/scalapb-validate/tree/master/example). 54 | 55 | [Link-SonatypeSnapshots]: https://oss.sonatype.org/content/repositories/snapshots/com/thesamet/scalapb/scalapb-validate-core_2.13/ "Sonatype Snapshots" 56 | [Badge-SonatypeSnapshots]: https://img.shields.io/nexus/s/https/oss.sonatype.org/com.thesamet.scalapb/scalapb-validate-core_2.13.svg "Sonatype Snapshots" 57 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Settings.stdSettings 2 | import scalapb.compiler.Version.scalapbVersion 3 | 4 | val Scala213 = "2.13.15" 5 | 6 | val Scala212 = "2.12.20" 7 | 8 | val Scala3 = "3.3.6" 9 | 10 | publish / skip := true 11 | 12 | sonatypeProfileName := "com.thesamet" 13 | 14 | def protobufJava = "com.google.protobuf" % "protobuf-java" % "3.25.8" 15 | 16 | inThisBuild( 17 | List( 18 | organization := "com.thesamet.scalapb", 19 | homepage := Some(url("https://github.com/scalapb/scalapb-validate")), 20 | licenses := List( 21 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 22 | ), 23 | developers := List( 24 | Developer( 25 | "thesamet", 26 | "Nadav Samet", 27 | "thesamet@gmail.com", 28 | url("https://www.thesamet.com") 29 | ) 30 | ), 31 | PB.protocVersion := protobufJava.revision 32 | ) 33 | ) 34 | 35 | val pgvVersion = "0.6.13" 36 | val munitSettings = Seq( 37 | libraryDependencies += "org.scalameta" %% "munit" % "1.0.3" % Test, 38 | testFrameworks += new TestFramework("munit.Framework") 39 | ) 40 | 41 | lazy val core = projectMatrix 42 | .in(file("core")) 43 | .defaultAxes() 44 | .settings(stdSettings) 45 | .settings(munitSettings) 46 | .settings( 47 | name := "scalapb-validate-core", 48 | libraryDependencies ++= Seq( 49 | "com.thesamet.scalapb.common-protos" %% "pgv-proto-scalapb_0.11" % (pgvVersion + "-0"), 50 | "com.thesamet.scalapb.common-protos" %% "pgv-proto-scalapb_0.11" % (pgvVersion + "-0") % "protobuf", 51 | "com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf" 52 | ), 53 | Compile / PB.targets := Seq( 54 | PB.gens.java -> (Compile / sourceManaged).value / "scalapb", 55 | scalapb.gen() -> (Compile / sourceManaged).value / "scalapb" 56 | ), 57 | Compile / packageBin / packageOptions += 58 | Package.ManifestAttributes( 59 | "ScalaPB-Options-Proto" -> 60 | "scalapb/validate-options.proto" 61 | ), 62 | compileOrder := CompileOrder.JavaThenScala 63 | ) 64 | .jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3)) 65 | 66 | lazy val cats = projectMatrix 67 | .in(file("cats")) 68 | .dependsOn(core) 69 | .defaultAxes() 70 | .settings(stdSettings) 71 | .settings(munitSettings) 72 | .settings( 73 | name := "scalapb-validate-cats", 74 | libraryDependencies ++= Seq( 75 | "org.typelevel" %% "cats-core" % "2.12.0" % "provided", 76 | "com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion % "provided" 77 | ) 78 | ) 79 | .jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3)) 80 | 81 | lazy val codeGen = projectMatrix 82 | .in(file("code-gen")) 83 | .defaultAxes() 84 | .enablePlugins(BuildInfoPlugin) 85 | .settings(stdSettings) 86 | .settings(munitSettings) 87 | .settings( 88 | buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion), 89 | buildInfoPackage := "scalapb.validate.compiler", 90 | name := "scalapb-validate-codegen", 91 | libraryDependencies ++= Seq( 92 | protobufJava, 93 | "com.thesamet.scalapb" %% "compilerplugin" % scalapbVersion, 94 | // scalapb-runtime does not gent automatically added since we do not have Scala gen, 95 | // and we want to make sure that a possibly older runtime (with different scalapb.proto) 96 | // gets in through the common-protos dependency. 97 | "com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion, 98 | "com.thesamet.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf", 99 | "com.thesamet.scalapb.common-protos" %% "pgv-proto-scalapb_0.11" % (pgvVersion + "-0") % "protobuf", 100 | "com.thesamet.scalapb.common-protos" %% "pgv-proto-scalapb_0.11" % (pgvVersion + "-0") 101 | ), 102 | Compile / PB.protoSources += core.base / "src" / "main" / "protobuf", 103 | Compile / PB.targets := Seq( 104 | PB.gens.java -> (Compile / sourceManaged).value / "scalapb" 105 | ), 106 | compileOrder := CompileOrder.JavaThenScala 107 | ) 108 | .jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3)) 109 | 110 | lazy val codeGenJVM212 = codeGen.jvm(Scala212) 111 | 112 | lazy val protocGenScalaPbValidate = 113 | protocGenProject("protoc-gen-scalapb-validate", codeGenJVM212) 114 | .settings( 115 | Compile / mainClass := Some("scalapb.validate.compiler.CodeGenerator") 116 | ) 117 | 118 | lazy val e2e = projectMatrix 119 | .in(file("e2e")) 120 | .defaultAxes() 121 | .dependsOn(core, cats) 122 | .enablePlugins(LocalCodeGenPlugin) 123 | .settings(stdSettings) 124 | .settings(munitSettings) 125 | .settings( 126 | run / baseDirectory := (LocalRootProject / baseDirectory).value, 127 | run / fork := true, 128 | publish / skip := true, 129 | crossScalaVersions := Seq(Scala212, Scala213), 130 | codeGenClasspath := (codeGenJVM212 / Compile / fullClasspath).value, 131 | libraryDependencies ++= Seq( 132 | "com.thesamet.scalapb" %% "scalapb-json4s" % "0.12.1", 133 | "org.typelevel" %% "cats-core" % "2.12.0", 134 | "io.undertow" % "undertow-core" % "2.3.18.Final", 135 | "eu.timepit" %% "refined" % "0.11.2", 136 | "io.envoyproxy.protoc-gen-validate" % "pgv-java-stub" % pgvVersion % "protobuf" 137 | ), 138 | TestProtosGenerator.generateAllTypesProtoSettings, 139 | Compile / PB.protoSources += (Compile / sourceManaged).value / "protobuf", 140 | Compile / PB.protoSources ++= (if (scalaVersion.value.startsWith("2.")) 141 | Seq( 142 | (Compile / sourceDirectory).value / "protobuf-scala2" 143 | ) 144 | else Seq.empty), 145 | Compile / PB.targets := Seq( 146 | genModule("scalapb.validate.compiler.ValidatePreprocessor$") -> 147 | (Compile / sourceManaged).value / "scalapb", 148 | scalapb.gen(grpc = true) -> (Compile / sourceManaged).value / "scalapb", 149 | genModule("scalapb.validate.compiler.CodeGenerator$") -> 150 | (Compile / sourceManaged).value / "scalapb" 151 | ) 152 | ) 153 | .jvmPlatform(scalaVersions = Seq(Scala212, Scala213, Scala3)) 154 | -------------------------------------------------------------------------------- /cats/src/main/scala/scalapb/validate/cats/CatsAdapters.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import cats.data.{NonEmptyList, NonEmptyMap, NonEmptySet} 4 | import scala.collection.immutable.SortedSet 5 | import scala.collection.immutable.SortedMap 6 | import scalapb.validate.ValidationException 7 | import scalapb.CollectionAdapter 8 | 9 | class NonEmptyListAdapter[T] extends CollectionAdapter[T, NonEmptyList[T]] { 10 | def foreach(coll: NonEmptyList[T])(f: T => Unit) = { coll.map(f); () } 11 | 12 | def empty: NonEmptyList[T] = 13 | throw new ValidationException( 14 | "No empty instance available for cats.Data.NonEmptyList" 15 | ) 16 | 17 | def newBuilder: Builder = 18 | List 19 | .newBuilder[T] 20 | .mapResult(list => 21 | NonEmptyList 22 | .fromList(list) 23 | .toRight( 24 | new ValidationException("Could not build an empty NonEmptyList") 25 | ) 26 | ) 27 | 28 | def concat(first: NonEmptyList[T], second: Iterable[T]) = 29 | first ++ second.toList 30 | 31 | def toIterator(value: NonEmptyList[T]): Iterator[T] = value.iterator 32 | 33 | def size(value: NonEmptyList[T]): Int = value.size 34 | } 35 | 36 | object NonEmptyListAdapter { 37 | def apply[T](): NonEmptyListAdapter[T] = new NonEmptyListAdapter[T] 38 | } 39 | 40 | class NonEmptySetAdapter[T: Ordering] 41 | extends CollectionAdapter[T, NonEmptySet[T]] { 42 | def foreach(coll: NonEmptySet[T])(f: T => Unit) = { coll.map(f); {} } 43 | 44 | def empty: NonEmptySet[T] = 45 | throw new ValidationException( 46 | "No empty instance available for cats.Data.NonEmptyList" 47 | ) 48 | 49 | def newBuilder: Builder = 50 | Vector 51 | .newBuilder[T] 52 | .mapResult(vec => 53 | if (vec.distinct.size != vec.size) 54 | Left( 55 | new ValidationException("Got duplicate elements for NonEmptySet") 56 | ) 57 | else 58 | NonEmptySet 59 | .fromSet(SortedSet(vec: _*)) 60 | .toRight( 61 | new ValidationException("Could not build an empty NonEmptySet") 62 | ) 63 | ) 64 | 65 | def concat(first: NonEmptySet[T], second: Iterable[T]) = 66 | throw new ValidationException( 67 | "No empty instance available for cats.Data.NonEmptyList" 68 | ) 69 | 70 | def toIterator(value: NonEmptySet[T]): Iterator[T] = 71 | value.toSortedSet.iterator 72 | 73 | def size(value: NonEmptySet[T]): Int = value.length 74 | } 75 | 76 | object NonEmptySetAdapter { 77 | def apply[T: Ordering](): NonEmptySetAdapter[T] = new NonEmptySetAdapter[T] 78 | } 79 | 80 | class NonEmptyMapAdapter[K: Ordering, V] 81 | extends CollectionAdapter[(K, V), NonEmptyMap[K, V]] { 82 | def foreach(coll: NonEmptyMap[K, V])(f: ((K, V)) => Unit) = 83 | coll.toSortedMap.foreach(f) 84 | 85 | def empty: NonEmptyMap[K, V] = 86 | throw new ValidationException( 87 | "No empty instance available for cats.Data.NonEmptyList" 88 | ) 89 | 90 | def newBuilder: Builder = 91 | SortedMap 92 | .newBuilder[K, V] 93 | .mapResult(map => 94 | NonEmptyMap 95 | .fromMap(map) 96 | .toRight( 97 | new ValidationException("Could not build an empty NonEmptyMap") 98 | ) 99 | ) 100 | 101 | def concat(first: NonEmptyMap[K, V], second: Iterable[(K, V)]) = 102 | NonEmptyMap.fromMapUnsafe(first.toSortedMap ++ second.toMap) 103 | 104 | def toIterator(value: NonEmptyMap[K, V]): Iterator[(K, V)] = 105 | value.toSortedMap.iterator 106 | 107 | def size(value: NonEmptyMap[K, V]): Int = value.length 108 | } 109 | 110 | object NonEmptyMapAdapter { 111 | def apply[K: Ordering, V](): NonEmptyMapAdapter[K, V] = 112 | new NonEmptyMapAdapter[K, V] 113 | } 114 | -------------------------------------------------------------------------------- /cats/src/main/scala/scalapb/validate/cats/NonEmptyChainAdapter.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import cats.Foldable 4 | import cats.data.Chain 5 | import cats.data.NonEmptyChain 6 | import scalapb.validate.ValidationException 7 | import scalapb.CollectionAdapter 8 | 9 | class NonEmptyChainAdapter[T] extends CollectionAdapter[T, NonEmptyChain[T]] { 10 | override def foreach(coll: NonEmptyChain[T])(f: T => Unit): Unit = 11 | coll.iterator.foreach(f) 12 | 13 | override def empty: NonEmptyChain[T] = 14 | throw new ValidationException( 15 | "No empty instance available for cats.Data.NonEmptyChain" 16 | ) 17 | 18 | override def newBuilder: Builder = 19 | List 20 | .newBuilder[T] 21 | .mapResult(list => 22 | NonEmptyChain 23 | .fromSeq(list) 24 | .toRight( 25 | new ValidationException("Could not build an empty NonEmptyChain") 26 | ) 27 | ) 28 | 29 | override def concat( 30 | first: NonEmptyChain[T], 31 | second: Iterable[T] 32 | ): NonEmptyChain[T] = 33 | first :++ Chain(second.toList: _*) 34 | 35 | override def toIterator(value: NonEmptyChain[T]): Iterator[T] = value.iterator 36 | 37 | override def size(value: NonEmptyChain[T]): Int = 38 | Foldable[NonEmptyChain].size(value).toInt 39 | } 40 | 41 | object NonEmptyChainAdapter { 42 | def apply[T](): NonEmptyChainAdapter[T] = new NonEmptyChainAdapter[T] 43 | } 44 | -------------------------------------------------------------------------------- /cats/src/main/scala/scalapb/validate/cats/NonEmptySeqAdapter.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import cats.Foldable 4 | import cats.data.NonEmptySeq 5 | import scalapb.validate.ValidationException 6 | import scalapb.CollectionAdapter 7 | 8 | class NonEmptySeqAdapter[T] extends CollectionAdapter[T, NonEmptySeq[T]] { 9 | override def foreach(coll: NonEmptySeq[T])(f: T => Unit): Unit = 10 | coll.iterator.foreach(f) 11 | 12 | override def empty: NonEmptySeq[T] = 13 | throw new ValidationException( 14 | "No empty instance available for cats.Data.NonEmptySeq" 15 | ) 16 | 17 | override def newBuilder: Builder = 18 | List 19 | .newBuilder[T] 20 | .mapResult(list => 21 | NonEmptySeq 22 | .fromSeq(list) 23 | .toRight( 24 | new ValidationException("Could not build an empty NonEmptySeq") 25 | ) 26 | ) 27 | 28 | override def concat( 29 | first: NonEmptySeq[T], 30 | second: Iterable[T] 31 | ): NonEmptySeq[T] = 32 | first.appendSeq(second.toList) 33 | 34 | override def toIterator(value: NonEmptySeq[T]): Iterator[T] = value.iterator 35 | 36 | override def size(value: NonEmptySeq[T]): Int = 37 | Foldable[NonEmptySeq].size(value).toInt 38 | } 39 | 40 | object NonEmptySeqAdapter { 41 | def apply[T](): NonEmptySeqAdapter[T] = new NonEmptySeqAdapter[T] 42 | } 43 | -------------------------------------------------------------------------------- /cats/src/main/scala/scalapb/validate/cats/NonEmptyVectorAdapter.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import cats.Foldable 4 | import cats.data.NonEmptyVector 5 | import scalapb.validate.ValidationException 6 | import scalapb.CollectionAdapter 7 | 8 | class NonEmptyVectorAdapter[T] extends CollectionAdapter[T, NonEmptyVector[T]] { 9 | override def foreach(coll: NonEmptyVector[T])(f: T => Unit): Unit = 10 | coll.iterator.foreach(f) 11 | 12 | override def empty: NonEmptyVector[T] = 13 | throw new ValidationException( 14 | "No empty instance available for cats.Data.NonEmptyVector" 15 | ) 16 | 17 | override def newBuilder: Builder = 18 | Vector 19 | .newBuilder[T] 20 | .mapResult(list => 21 | NonEmptyVector 22 | .fromVector(list) 23 | .toRight( 24 | new ValidationException("Could not build an empty NonEmptyVector") 25 | ) 26 | ) 27 | 28 | override def concat( 29 | first: NonEmptyVector[T], 30 | second: Iterable[T] 31 | ): NonEmptyVector[T] = 32 | first.appendVector(second.toVector) 33 | 34 | override def toIterator(value: NonEmptyVector[T]): Iterator[T] = 35 | value.iterator 36 | 37 | override def size(value: NonEmptyVector[T]): Int = 38 | Foldable[NonEmptyVector].size(value).toInt 39 | } 40 | 41 | object NonEmptyVectorAdapter { 42 | def apply[T](): NonEmptyVectorAdapter[T] = new NonEmptyVectorAdapter[T] 43 | } 44 | -------------------------------------------------------------------------------- /code-gen/src/main/scala-2.12/scalapb/validate/compat.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | object compat { 4 | val JavaConverters = collection.JavaConverters 5 | } 6 | -------------------------------------------------------------------------------- /code-gen/src/main/scala-2.13/scalapb/validate/compat.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | object compat { 4 | val JavaConverters = scala.jdk.CollectionConverters 5 | } 6 | -------------------------------------------------------------------------------- /code-gen/src/main/scala-3/scalapb/validate/compat.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | object compat { 4 | val JavaConverters = scala.jdk.CollectionConverters 5 | } 6 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/BooleanRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import io.envoyproxy.pgv.validate.validate.BoolRules 4 | 5 | object BooleanRulesGen { 6 | def booleanRules( 7 | rules: BoolRules 8 | ): Seq[Rule] = 9 | Seq( 10 | rules.const.map(v => ComparativeRulesGen.constRule(v.toString())) 11 | ).flatten 12 | } 13 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/BytesRuleGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import com.google.protobuf.ByteString 4 | import com.google.protobuf.Descriptors.FieldDescriptor 5 | import io.envoyproxy.pgv.validate.validate.BytesRules 6 | import scalapb.validate.compiler.Rule.ifSet 7 | import scalapb.validate.compiler.StringRulesGen.quoted 8 | 9 | object BytesRuleGen { 10 | private val BV: String = "io.envoyproxy.pgv.BytesValidation" 11 | private val CV: String = "io.envoyproxy.pgv.ConstantValidation" 12 | 13 | def preamble(pname: String, v: ByteString) = 14 | s"private val $pname: Array[Byte] = Array[Byte](${v.toByteArray.map(_.toString).mkString(", ")})" 15 | 16 | def preambleByteString(pname: String, v: ByteString) = 17 | s"private val $pname: com.google.protobuf.ByteString = com.google.protobuf.ByteString.copyFrom(Array[Byte](${v.toByteArray.map(_.toString).mkString(", ")}))" 18 | 19 | def maxLength(value: Long): FunctionCall = 20 | Rule.java(s"$BV.maxLength", value.toString) 21 | 22 | def bytesRules( 23 | fd: FieldDescriptor, 24 | rules: BytesRules 25 | ): Seq[Rule] = 26 | Seq( 27 | rules.len.map(value => Rule.java(s"$BV.length", value.toString)), 28 | rules.minLen.map(value => Rule.java(s"$BV.minLength", value.toString)), 29 | rules.maxLen.map(value => maxLength(value)), 30 | rules.const.map { 31 | val pname = s"Const_${fd.getName}" 32 | v => 33 | Rule 34 | .java(s"$CV.constant", pname) 35 | .withPreamble( 36 | preambleByteString(pname, v) 37 | ) 38 | }, 39 | rules.prefix.map { (v: ByteString) => 40 | val pname = s"Prefix_${fd.getName}" 41 | Rule 42 | .java( 43 | s"$BV.prefix", 44 | pname 45 | ) 46 | .withPreamble( 47 | preamble(pname, v) 48 | ) 49 | }, 50 | rules.suffix.map { v => 51 | val pname = s"Suffix_${fd.getName}" 52 | Rule 53 | .java( 54 | s"$BV.suffix", 55 | pname 56 | ) 57 | .withPreamble( 58 | preamble(pname, v) 59 | ) 60 | }, 61 | rules.pattern.map { v => 62 | val pname = s"Pattern_${fd.getName}" 63 | Rule 64 | .java( 65 | s"$BV.pattern", 66 | pname 67 | ) 68 | .withPreamble( 69 | s"private val $pname = com.google.re2j.Pattern.compile(${quoted(v)})" 70 | ) 71 | }, 72 | ifSet(rules.getIp)(Rule.java(s"$BV.ip")), 73 | ifSet(rules.getIpv4)(Rule.java(s"$BV.ipv4")), 74 | ifSet(rules.getIpv6)(Rule.java(s"$BV.ipv6")), 75 | rules.contains.map { v => 76 | val pname = s"Contains_${fd.getName}" 77 | Rule 78 | .java( 79 | s"$BV.contains", 80 | pname 81 | ) 82 | .withPreamble( 83 | preamble(pname, v) 84 | ) 85 | } 86 | ).flatten ++ MembershipRulesGen.membershipRules(rules) 87 | } 88 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/ComparativeRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import scala.language.reflectiveCalls 4 | import scala.math.Ordering.Implicits._ 5 | import com.google.protobuf.timestamp.Timestamp 6 | import Rule.basic 7 | import com.google.protobuf.duration.Duration 8 | import scala.reflect.ClassTag 9 | import scala.reflect.classTag 10 | 11 | object ComparativeRulesGen { 12 | val TimestampOrdering = 13 | "scalapb.validate.ComparativeValidation.timestampOrdering" 14 | val DurationOrdering = 15 | "scalapb.validate.ComparativeValidation.durationOrdering" 16 | 17 | type ComparativeRules[T] = { 18 | def lte: Option[T] 19 | def lt: Option[T] 20 | def gt: Option[T] 21 | def gte: Option[T] 22 | def const: Option[T] 23 | } 24 | 25 | type NumericRules[T] = ComparativeRules[T] 26 | with MembershipRulesGen.MembershipRules[T] 27 | 28 | def numericRules[T: Ordering: Show: ClassTag]( 29 | rules: NumericRules[T] 30 | ): Seq[Rule] = 31 | comparativeRules(rules) ++ MembershipRulesGen.membershipRules[T]( 32 | rules 33 | ) 34 | 35 | def additionalImports(className: String): Seq[String] = 36 | className match { 37 | case "com.google.protobuf.timestamp.Timestamp" => Seq(TimestampOrdering) 38 | case "com.google.protobuf.duration.Duration" => Seq(DurationOrdering) 39 | case _ => Nil 40 | } 41 | 42 | // constant definition 43 | private[validate] val CV = "scalapb.validate.ComparativeValidation" 44 | 45 | def constRule(const: String) = 46 | basic(s"$CV.constant", const) 47 | 48 | def comparativeRules[T: Ordering: ClassTag]( 49 | rules: ComparativeRules[T] 50 | )(implicit show: Show[T]): Seq[FunctionCall] = { 51 | if (rules.gt.isDefined && rules.gte.isDefined) 52 | new RuntimeException("Error: both gt and gte were specified.") 53 | if (rules.lt.isDefined && rules.lte.isDefined) 54 | new RuntimeException("Error: both lt and lte were specified.") 55 | val gtType = 56 | if (rules.gt.isDefined) "Gt" 57 | else if (rules.gte.isDefined) "Gte" 58 | else "" 59 | val ltType = 60 | if (rules.lt.isDefined) "Lt" 61 | else if (rules.lte.isDefined) "Lte" 62 | else "" 63 | val maybeGtVal = rules.gt.orElse(rules.gte) 64 | val maybeLtVal = rules.lt.orElse(rules.lte) 65 | 66 | val constRules = Seq( 67 | rules.const.map(v => constRule(show(v))) 68 | ).flatten 69 | 70 | val rangeRules = (maybeGtVal, maybeLtVal) match { 71 | case (Some(gtVal), Some(ltVal)) => 72 | val ex = if (ltVal < gtVal) "Ex" else "" 73 | Seq( 74 | basic( 75 | s"$CV.range$gtType$ltType$ex", 76 | args = Seq( 77 | show(gtVal), 78 | show(ltVal) 79 | ) 80 | ) 81 | ) 82 | case _ => 83 | Seq( 84 | rules.gt.map(v => basic(s"$CV.greaterThan", show(v))), 85 | rules.gte.map(v => basic(s"$CV.greaterThanOrEqual", show(v))), 86 | rules.lt 87 | .map(v => basic(s"$CV.lessThan", show(v))), 88 | rules.lte.map(v => basic(s"$CV.lessThanOrEqual", show(v))) 89 | ).flatten 90 | } 91 | 92 | val imports = additionalImports(classTag[T].runtimeClass.getName) 93 | (rangeRules ++ constRules).map(rule => 94 | imports.foldLeft[FunctionCall](rule)(_.withImport(_)) 95 | ) 96 | } 97 | 98 | implicit val timestampOrdering: Ordering[Timestamp] = 99 | new Ordering[Timestamp] { 100 | def compare(x: Timestamp, y: Timestamp): Int = { 101 | val o1 = java.lang.Long.compare(x.seconds, y.seconds) 102 | if (o1 != 0) o1 103 | else java.lang.Integer.compare(x.nanos, y.nanos) 104 | } 105 | } 106 | 107 | implicit val durationOrdering: Ordering[Duration] = new Ordering[Duration] { 108 | def compare(x: Duration, y: Duration): Int = { 109 | val o1 = java.lang.Long.compare(x.seconds, y.seconds) 110 | if (o1 != 0) o1 111 | else java.lang.Integer.compare(x.nanos, y.nanos) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/EnumRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import io.envoyproxy.pgv.validate.validate.EnumRules 4 | import scalapb.compiler.MethodApplication 5 | import Rule.ifSet 6 | 7 | object EnumRulesGen { 8 | private[validate] val EV = "scalapb.validate.EnumValidation" 9 | private[validate] val CV = "scalapb.validate.ComparativeValidation" 10 | 11 | def enumRules( 12 | rules: EnumRules 13 | ): Seq[Rule] = 14 | MembershipRulesGen.membershipRules( 15 | rules, 16 | transform = MethodApplication("value") 17 | ) ++ 18 | Seq( 19 | ifSet(rules.getDefinedOnly)(Rule.basic(s"$EV.definedOnly")), 20 | rules.const.map(c => 21 | Rule.basic( 22 | s"$CV.constant", 23 | Seq(c.toString), 24 | inputTransform = MethodApplication("value") 25 | ) 26 | ) 27 | ).flatten 28 | } 29 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/IgnoreEmptyRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import com.google.protobuf.Descriptors.FieldDescriptor 4 | import io.envoyproxy.pgv.validate.validate.FieldRules 5 | import io.envoyproxy.pgv.validate.validate.FieldRules.Type 6 | import scalapb.compiler.DescriptorImplicits 7 | import scala.language.reflectiveCalls 8 | import ComparativeRulesGen.NumericRules 9 | import scala.reflect.{classTag, ClassTag} 10 | 11 | object IgnoreEmptyRulesGen { 12 | type HasIgnoreEmpty = { 13 | def getIgnoreEmpty: Boolean 14 | } 15 | 16 | def numericIsEmpty[T: ClassTag]( 17 | rules: NumericRules[T] with HasIgnoreEmpty 18 | ): Option[Rule] = { 19 | val zero = classTag[T] match { 20 | case ct if ct == classTag[Long] => "0L" 21 | case ct if ct == classTag[Int] => "0" 22 | case ct if ct == classTag[Double] => "0.0" 23 | case ct if ct == classTag[Float] => "0.0f" 24 | case ct => 25 | throw new RuntimeException(s"Unsupported numeric field type $ct") 26 | } 27 | Rule.ifSet(rules.getIgnoreEmpty)(ComparativeRulesGen.constRule(zero)) 28 | } 29 | 30 | def ignoreEmptyRule( 31 | fd: FieldDescriptor, 32 | rules: FieldRules, 33 | di: DescriptorImplicits 34 | ): Option[Rule] = 35 | rules.`type` match { 36 | case Type.Repeated(rules) => 37 | Rule.ifSet(rules.getIgnoreEmpty)(RepeatedRulesGen.maxItems(fd, 0, di)) 38 | 39 | case Type.String(rules) => 40 | Rule.ifSet(rules.getIgnoreEmpty)(StringRulesGen.maxLen(0)) 41 | 42 | case Type.Bytes(rules) => 43 | Rule.ifSet(rules.getIgnoreEmpty)(BytesRuleGen.maxLength(0)) 44 | 45 | case Type.Uint64(numericRules) => 46 | numericIsEmpty(numericRules) 47 | 48 | case Type.Sint64(numericRules) => 49 | numericIsEmpty(numericRules) 50 | 51 | case Type.Sfixed64(numericRules) => 52 | numericIsEmpty(numericRules) 53 | 54 | case Type.Fixed64(numericRules) => 55 | numericIsEmpty(numericRules) 56 | 57 | case Type.Int64(numericRules) => 58 | numericIsEmpty(numericRules) 59 | 60 | case Type.Uint32(numericRules) => 61 | numericIsEmpty(numericRules) 62 | 63 | case Type.Sint32(numericRules) => 64 | numericIsEmpty(numericRules) 65 | 66 | case Type.Sfixed32(numericRules) => 67 | numericIsEmpty(numericRules) 68 | 69 | case Type.Fixed32(numericRules) => 70 | numericIsEmpty(numericRules) 71 | 72 | case Type.Int32(numericRules) => 73 | numericIsEmpty(numericRules) 74 | 75 | case Type.Double(numericRules) => 76 | numericIsEmpty(numericRules) 77 | 78 | case Type.Float(numericRules) => 79 | numericIsEmpty(numericRules) 80 | 81 | case Type.Timestamp(_) => 82 | None 83 | 84 | case Type.Duration(_) => 85 | None 86 | 87 | case Type.Bool(_) => 88 | None 89 | 90 | case Type.Any(_) => 91 | None 92 | 93 | case Type.Map(mapRules) => 94 | Rule.ifSet(mapRules.getIgnoreEmpty)( 95 | RepeatedRulesGen.maxItems(fd, 0, di) 96 | ) 97 | 98 | case Type.Enum(_) => 99 | None 100 | 101 | case _ => None 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/MapRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import scalapb.compiler.DescriptorImplicits 4 | import io.envoyproxy.pgv.validate.validate.MapRules 5 | import com.google.protobuf.Descriptors.FieldDescriptor 6 | 7 | object MapRulesGen { 8 | val RR = "scalapb.validate.MapValidation" 9 | 10 | def mapRules( 11 | field: FieldDescriptor, 12 | rules: MapRules, 13 | desriptorImplicits: DescriptorImplicits 14 | ): Seq[Rule] = { 15 | import desriptorImplicits._ 16 | Seq( 17 | rules.minPairs.map(value => 18 | Rule.basic( 19 | s"$RR.minPairs", 20 | Seq(value.toString), 21 | inputTransform = field.collection.size 22 | ) 23 | ), 24 | rules.maxPairs.map(value => 25 | Rule.basic( 26 | s"$RR.maxPairs", 27 | Seq(value.toString), 28 | inputTransform = field.collection.size 29 | ) 30 | ), 31 | Rule.ifSet(rules.getNoSparse)(Rule.basic(s"$RR.notSparse")) 32 | ).flatten 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/MembershipRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import scalapb.compiler.{Expression, Identity} 4 | import scalapb.validate.compiler.Rule._ 5 | 6 | import scala.language.reflectiveCalls 7 | import scala.reflect.{classTag, ClassTag} 8 | 9 | object MembershipRulesGen { 10 | private val MV: String = "scalapb.validate.MembershipValidation" 11 | 12 | type MembershipRules[T] = { 13 | def in: Seq[T] 14 | def notIn: Seq[T] 15 | } 16 | 17 | def membershipRules[T: ClassTag]( 18 | rules: MembershipRules[T], 19 | transform: Expression = Identity 20 | )(implicit show: Show[T]) = { 21 | val runtimeClass = classTag[T].runtimeClass 22 | val className = 23 | if (runtimeClass.isPrimitive()) runtimeClass.getName() match { 24 | case "int" => "Int" 25 | case "long" => "Long" 26 | case "float" => "Float" 27 | case "double" => "Double" 28 | } 29 | else runtimeClass.getName() 30 | 31 | Seq( 32 | if (rules.in.nonEmpty) 33 | Some( 34 | basic( 35 | s"$MV.in[$className]", 36 | Seq(rules.in.map(v => show(v)).mkString("Seq(", ", ", ")")), 37 | transform 38 | ) 39 | ) 40 | else None, 41 | if (rules.notIn.nonEmpty) 42 | Some( 43 | basic( 44 | s"$MV.notIn[$className]", 45 | Seq(rules.notIn.map(v => show(v)).mkString("Seq(", ", ", ")")), 46 | transform 47 | ) 48 | ) 49 | else None 50 | ).flatten 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/RepeatedRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import io.envoyproxy.pgv.validate.validate.RepeatedRules 4 | import scalapb.compiler.DescriptorImplicits 5 | import com.google.protobuf.Descriptors.FieldDescriptor 6 | import scalapb.compiler.MethodApplication 7 | 8 | object RepeatedRulesGen { 9 | val RR = "scalapb.validate.RepeatedValidation" 10 | 11 | def maxItems( 12 | fd: FieldDescriptor, 13 | value: Long, 14 | desriptorImplicits: DescriptorImplicits 15 | ): FunctionCall = { 16 | import desriptorImplicits._ 17 | Rule.basic( 18 | s"$RR.maxItems", 19 | Seq(value.toString), 20 | inputTransform = fd.collection.size 21 | ) 22 | } 23 | 24 | def minItems( 25 | fd: FieldDescriptor, 26 | value: Long, 27 | desriptorImplicits: DescriptorImplicits 28 | ): FunctionCall = { 29 | import desriptorImplicits._ 30 | Rule.basic( 31 | s"$RR.minItems", 32 | Seq(value.toString), 33 | inputTransform = fd.collection.size 34 | ) 35 | } 36 | 37 | def repeatedRules( 38 | field: FieldDescriptor, 39 | rules: RepeatedRules, 40 | desriptorImplicits: DescriptorImplicits 41 | ): Seq[Rule] = { 42 | import desriptorImplicits._ 43 | Seq( 44 | rules.minItems.map(value => minItems(field, value, desriptorImplicits)), 45 | rules.maxItems.map(value => maxItems(field, value, desriptorImplicits)), 46 | Rule.ifSet( 47 | rules.getUnique /* && !field 48 | .fieldOptions 49 | .getExtension(scalapb.validate.Validate.field) 50 | .getSkipUniqueCheck() */ 51 | )( 52 | Rule.basic( 53 | s"$RR.unique", 54 | Seq(), 55 | inputTransform = 56 | field.collection.iterator.andThen(MethodApplication("toSeq")) 57 | ) 58 | ) 59 | ).flatten 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/RequiredRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | object RequiredRulesGen { 4 | val requiredRule = Rule.basic("scalapb.validate.RequiredValidation") 5 | } 6 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/Rule.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import com.google.protobuf.Descriptors.FieldDescriptor 4 | import scalapb.compiler.Expression 5 | import scalapb.compiler.Identity 6 | import scalapb.compiler.EnclosingType 7 | import scalapb.compiler.FunctionApplication 8 | import scalapb.compiler.FunctionalPrinter 9 | import scalapb.compiler.FunctionalPrinter.PrinterEndo 10 | 11 | trait Rule { 12 | def preamble: Seq[String] 13 | def imports: Seq[String] 14 | def render(descriptor: FieldDescriptor, input: String): PrinterEndo 15 | } 16 | 17 | /** Represents a generated function call that returns a Result. 18 | * 19 | * funcName: fully qualified functin name. The function takes potentially the 20 | * name (if needsName is true), then the value to be tested, then the list of 21 | * args are passed: funcName([name], value, *args) needsName: whether the first 22 | * argument is the name of the field being tested. args: arguments to pass 23 | * after the value. inputTransform: transformation to apply to the value 24 | * outputTransform: transformation to apply to the result of funcName imports: 25 | * list of imports to add to the top of the file (no need to dedupe here) 26 | * preamble: code to be added for static definitions. Can be used for constants 27 | * that needs to be computed only once. 28 | */ 29 | case class FunctionCall( 30 | funcName: String, 31 | needsName: Boolean, 32 | args: Seq[String], 33 | inputTransform: Expression, 34 | outputTranform: Expression, 35 | imports: Seq[String], 36 | preamble: Seq[String] 37 | ) extends Rule { 38 | self => 39 | def render(descriptor: FieldDescriptor, input: String): PrinterEndo = { 40 | val e = 41 | inputTransform(input, EnclosingType.None, false) 42 | val maybeName = 43 | if (needsName) s""""${Rule.getFullNameWithoutPackage(descriptor)}", """ 44 | else "" 45 | val base = s"""$funcName($maybeName$e""" 46 | val out = 47 | if (args.isEmpty) base + ")" 48 | else base + args.mkString(", ", ", ", ")") 49 | _.add(outputTranform(out, EnclosingType.None, false)) 50 | } 51 | 52 | def wrapJava: FunctionCall = 53 | copy(outputTranform = 54 | FunctionApplication("scalapb.validate.Result.run") andThen outputTranform 55 | ) 56 | 57 | def withImport(name: String) = copy(imports = imports :+ name) 58 | 59 | def withPreamble(lines: String*) = copy(preamble = preamble ++ lines) 60 | } 61 | 62 | case class CombineFieldRules(rules: Seq[Rule], op: String = "&&") extends Rule { 63 | def preamble: Seq[String] = rules.flatMap(_.preamble) 64 | def imports: Seq[String] = rules.flatMap(_.imports) 65 | 66 | def render(descriptor: FieldDescriptor, input: String): PrinterEndo = { 67 | val lines = rules.map { r => 68 | val fp = FunctionalPrinter() 69 | r.render(descriptor, input)(fp).content 70 | } 71 | _.addGroupsWithDelimiter(op)(lines) 72 | } 73 | } 74 | 75 | case class IgnoreEmptyRule(isEmpty: Rule, other: Rule) extends Rule { 76 | def preamble: Seq[String] = isEmpty.preamble ++ other.preamble 77 | def imports: Seq[String] = isEmpty.imports ++ other.imports 78 | def render(descriptor: FieldDescriptor, input: String): PrinterEndo = 79 | _.add("(") 80 | .indented( 81 | _.call(isEmpty.render(descriptor, input)) 82 | .add(" ||") 83 | .call(other.render(descriptor, input)) 84 | .add(")") 85 | ) 86 | } 87 | 88 | case class OptionalFieldRule(rules: Seq[Rule]) extends Rule { 89 | def render(descriptor: FieldDescriptor, input: String): PrinterEndo = { 90 | val lines = rules.map { r => 91 | val fp = FunctionalPrinter() 92 | r.render(descriptor, "_value")(fp).content 93 | } 94 | 95 | _.add( 96 | s"scalapb.validate.Result.optional($input) { _value =>" 97 | ).indented(_.addGroupsWithDelimiter(" &&")(lines)) 98 | .add("}") 99 | } 100 | 101 | def preamble: Seq[String] = rules.flatMap(_.preamble) 102 | def imports: Seq[String] = rules.flatMap(_.imports) 103 | } 104 | 105 | case class RepeatedFieldRule(rules: Seq[Rule], inputTransform: String => String) 106 | extends Rule { 107 | def render(descriptor: FieldDescriptor, input: String): PrinterEndo = { 108 | val lines = rules.map { r => 109 | val fp = FunctionalPrinter() 110 | r.render(descriptor, "_value")(fp).content 111 | } 112 | _.add( 113 | s"scalapb.validate.Result.repeated(${inputTransform(input)}) { _value =>" 114 | ).indented(_.addGroupsWithDelimiter(" &&")(lines)) 115 | .add("}") 116 | } 117 | 118 | def preamble: Seq[String] = rules.flatMap(_.preamble) 119 | def imports: Seq[String] = rules.flatMap(_.imports) 120 | } 121 | 122 | object Rule { 123 | def basic( 124 | funcName: String, 125 | args: Seq[String], 126 | inputTransform: Expression = Identity 127 | ): FunctionCall = 128 | FunctionCall(funcName, true, args, inputTransform, Identity, Nil, Nil) 129 | 130 | def basic(funcName: String, args: String*): FunctionCall = 131 | basic(funcName, args) 132 | 133 | def java( 134 | funcName: String, 135 | args: Seq[String], 136 | inputTransform: Expression 137 | ): FunctionCall = 138 | basic(funcName, args, inputTransform).wrapJava 139 | 140 | def java(funcName: String, args: String*): FunctionCall = 141 | basic(funcName, args, Identity).wrapJava 142 | 143 | def messageValidate(validator: String): Rule = 144 | FunctionCall( 145 | s"$validator.validate", 146 | false, 147 | Seq.empty, 148 | Identity, 149 | Identity, 150 | Nil, 151 | Nil 152 | ) 153 | 154 | def ifSet[T](cond: => Boolean)(value: => T): Option[T] = 155 | if (cond) Some(value) else None 156 | 157 | private[compiler] def getFullNameWithoutPackage( 158 | descriptor: FieldDescriptor 159 | ): String = { 160 | val fullName = descriptor.getFullName 161 | val packageName = descriptor.getFile.getPackage 162 | if (packageName.isEmpty) 163 | fullName 164 | else 165 | fullName.substring(packageName.length + 1) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/RulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import com.google.protobuf.Descriptors.FieldDescriptor 4 | import io.envoyproxy.pgv.validate.validate.FieldRules 5 | import io.envoyproxy.pgv.validate.validate.FieldRules.Type 6 | import scalapb.compiler.MethodApplication 7 | import scalapb.validate.compiler.ComparativeRulesGen.{ 8 | durationOrdering, 9 | timestampOrdering 10 | } 11 | import scalapb.compiler.DescriptorImplicits 12 | 13 | object RulesGen { 14 | def rulesSingle( 15 | fd: FieldDescriptor, 16 | rules: FieldRules, 17 | implicits: DescriptorImplicits 18 | ): Seq[Rule] = 19 | rules.`type` match { 20 | case Type.String(stringRules) => 21 | StringRulesGen.stringRules(fd, stringRules) 22 | 23 | case Type.Bytes(bytesRules) => BytesRuleGen.bytesRules(fd, bytesRules) 24 | 25 | case Type.Uint64(numericRules) => 26 | ComparativeRulesGen.numericRules(numericRules) 27 | 28 | case Type.Sint64(numericRules) => 29 | ComparativeRulesGen.numericRules(numericRules) 30 | 31 | case Type.Sfixed64(numericRules) => 32 | ComparativeRulesGen.numericRules(numericRules) 33 | 34 | case Type.Fixed64(numericRules) => 35 | ComparativeRulesGen.numericRules(numericRules) 36 | 37 | case Type.Int64(numericRules) => 38 | ComparativeRulesGen.numericRules(numericRules) 39 | 40 | case Type.Uint32(numericRules) => 41 | ComparativeRulesGen.numericRules(numericRules) 42 | 43 | case Type.Sint32(numericRules) => 44 | ComparativeRulesGen.numericRules(numericRules) 45 | 46 | case Type.Sfixed32(numericRules) => 47 | ComparativeRulesGen.numericRules(numericRules) 48 | 49 | case Type.Fixed32(numericRules) => 50 | ComparativeRulesGen.numericRules(numericRules) 51 | 52 | case Type.Int32(numericRules) => 53 | ComparativeRulesGen.numericRules(numericRules) 54 | 55 | case Type.Double(numericRules) => 56 | ComparativeRulesGen.numericRules(numericRules) 57 | 58 | case Type.Float(numericRules) => 59 | ComparativeRulesGen.numericRules(numericRules) 60 | 61 | case Type.Timestamp(timestampRules) => 62 | ComparativeRulesGen.comparativeRules(timestampRules) ++ 63 | TimestampRulesGen.timestampRules(timestampRules) 64 | 65 | case Type.Duration(durationRules) => 66 | ComparativeRulesGen.numericRules(durationRules) 67 | 68 | case Type.Bool(boolRules) => 69 | BooleanRulesGen.booleanRules(boolRules) 70 | 71 | case Type.Repeated(repeatedRules) => 72 | RepeatedRulesGen.repeatedRules(fd, repeatedRules, implicits) 73 | 74 | case Type.Any(anyRules) => 75 | MembershipRulesGen.membershipRules( 76 | anyRules, 77 | MethodApplication("typeUrl") 78 | ) 79 | 80 | case Type.Map(mapRules) => 81 | MapRulesGen.mapRules(fd, mapRules, implicits) 82 | 83 | case Type.Enum(enumRules) => 84 | EnumRulesGen.enumRules(enumRules) 85 | 86 | case _ => Seq.empty 87 | } 88 | 89 | def isRequired(rules: FieldRules): Boolean = 90 | rules.getMessage.getRequired || (rules.`type` match { 91 | case Type.Timestamp(v) => v.getRequired 92 | case Type.Duration(v) => v.getRequired 93 | case Type.Any(v) => v.getRequired 94 | case _ => false 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/Show.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import com.google.protobuf.duration.Duration 4 | import com.google.protobuf.timestamp.Timestamp 5 | import com.google.protobuf.ByteString 6 | 7 | trait Show[T] { 8 | def apply(v: T): String 9 | } 10 | 11 | object Show { 12 | implicit val showFloat: Show[Float] = (v: Float) => s"${v}f" 13 | implicit val showDouble: Show[Double] = (v: Double) => s"$v" 14 | implicit val showInt: Show[Int] = (v: Int) => v.toString() 15 | implicit val showLong: Show[Long] = (v: Long) => s"${v}L" 16 | implicit val showString: Show[String] = (v: String) => 17 | StringRulesGen.quoted(v) 18 | implicit val showTimestamp: Show[Timestamp] = (v: Timestamp) => 19 | s"com.google.protobuf.timestamp.Timestamp.of(${v.seconds}L, ${v.nanos})" 20 | implicit val showDuration: Show[Duration] = (v: Duration) => 21 | s"com.google.protobuf.duration.Duration.of(${v.seconds}L, ${v.nanos})" 22 | implicit val showByteString: Show[ByteString] = (v: ByteString) => 23 | s"com.google.protobuf.ByteString.copyFrom(Array[Byte](${v.toByteArray.map(_.toString).mkString(", ")}))" 24 | } 25 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/StringRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import io.envoyproxy.pgv.validate.validate.StringRules 4 | import Rule._ 5 | import com.google.protobuf.Descriptors.FieldDescriptor 6 | 7 | /** StringRulesGenerator helps generate the validation code for protocol buffer 8 | * string typed field 9 | */ 10 | object StringRulesGen { 11 | private val SV: String = "io.envoyproxy.pgv.StringValidation" 12 | private val CV: String = "io.envoyproxy.pgv.ConstantValidation" 13 | private val WRX: String = "scalapb.validate.WellKnownRegex" 14 | 15 | def maxLen(value: Long): FunctionCall = 16 | Rule.java(s"$SV.maxLength", value.toString) 17 | 18 | // Copied from ScalaPB's ProtobufGenerator 19 | // TODO: move to common place 20 | private[validate] def quoted(raw: String): String = 21 | raw 22 | .map { 23 | case '\b' => "\\b" 24 | case '\f' => "\\f" 25 | case '\n' => "\\n" 26 | case '\r' => "\\r" 27 | case '\t' => "\\t" 28 | case '\\' => "\\\\" 29 | case '\"' => "\\\"" 30 | case '\'' => "\\\'" 31 | case u if u >= ' ' && u <= '~' => u.toString 32 | case c: Char => "\\u%4s".format(c.toInt.toHexString).replace(' ', '0') 33 | } 34 | .mkString("\"", "", "\"") 35 | 36 | def stringRules( 37 | fd: FieldDescriptor, 38 | rules: StringRules 39 | ): Seq[Rule] = 40 | Seq( 41 | rules.len.map(value => Rule.java(s"$SV.length", value.toString)), 42 | rules.lenBytes.map(value => Rule.java(s"$SV.lenBytes", value.toString)), 43 | rules.minLen.map(value => Rule.java(s"$SV.minLength", value.toString)), 44 | rules.maxLen.map(value => maxLen(value)), 45 | rules.minBytes.map(value => Rule.java(s"$SV.minBytes", value.toString)), 46 | rules.maxBytes.map(value => Rule.java(s"$SV.maxBytes", value.toString)), 47 | rules.contains.map(v => Rule.java(s"$SV.contains", quoted(v))), 48 | rules.notContains.map(v => Rule.java(s"$SV.notContains", quoted(v))), 49 | rules.const.map(v => Rule.java(s"$CV.constant", quoted(v))), 50 | rules.prefix.map(v => Rule.java(s"$SV.prefix", quoted(v))), 51 | rules.suffix.map(v => Rule.java(s"$SV.suffix", quoted(v))), 52 | rules.pattern.map { v => 53 | val pname = s"Pattern_${fd.getName()}" 54 | Rule 55 | .java( 56 | s"$SV.pattern", 57 | pname 58 | ) 59 | .withPreamble( 60 | s"private val $pname = com.google.re2j.Pattern.compile(${quoted(v)})" 61 | ) 62 | }, 63 | ifSet(rules.getAddress)(Rule.java(s"$SV.address")), 64 | ifSet(rules.getEmail)(Rule.java(s"$SV.email")), 65 | ifSet(rules.getHostname)(Rule.java(s"$SV.hostName")), 66 | ifSet(rules.getIp)(Rule.java(s"$SV.ip")), 67 | ifSet(rules.getIpv4)(Rule.java(s"$SV.ipv4")), 68 | ifSet(rules.getIpv6)(Rule.java(s"$SV.ipv6")), 69 | ifSet(rules.getUri)(Rule.java(s"$SV.uri")), 70 | ifSet(rules.getUriRef)(Rule.java(s"$SV.uriRef")), 71 | ifSet(rules.getUuid)(Rule.java(s"$SV.uuid")), 72 | ifSet(rules.getWellKnownRegex.isHttpHeaderName && rules.getStrict)( 73 | Rule.java(s"$SV.pattern", s"$WRX.HTTP_HEADER_NAME") 74 | ), 75 | ifSet(rules.getWellKnownRegex.isHttpHeaderValue && rules.getStrict)( 76 | Rule.java(s"$SV.pattern", s"$WRX.HTTP_HEADER_VALUE") 77 | ), 78 | ifSet( 79 | (rules.getWellKnownRegex.isHttpHeaderValue || rules.getWellKnownRegex.isHttpHeaderName) && !rules.getStrict 80 | )( 81 | Rule.java(s"$SV.pattern", s"$WRX.HEADER_STRING") 82 | ) 83 | ).flatten ++ MembershipRulesGen.membershipRules(rules) 84 | } 85 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/compiler/TimestampRulesGen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.compiler 2 | 3 | import io.envoyproxy.pgv.validate.validate.TimestampRules 4 | import scalapb.compiler.FunctionApplication 5 | 6 | object TimestampRulesGen { 7 | val TV = "scalapb.validate.TimestampValidation" 8 | def timestampRules(rules: TimestampRules) = 9 | Seq( 10 | rules.within.map { d => 11 | Rule.java( 12 | "io.envoyproxy.pgv.TimestampValidation.within", 13 | Seq( 14 | s"io.envoyproxy.pgv.TimestampValidation.toDuration(${d.seconds}, ${d.nanos})", 15 | "io.envoyproxy.pgv.TimestampValidation.currentTimestamp()" 16 | ), 17 | FunctionApplication( 18 | "com.google.protobuf.timestamp.Timestamp.toJavaProto" 19 | ) 20 | ) 21 | }, 22 | Rule.ifSet(rules.getLtNow)( 23 | Rule 24 | .basic( 25 | ComparativeRulesGen.CV + ".lessThan", 26 | s"$TV.currentTimestamp()" 27 | ) 28 | .withImport(ComparativeRulesGen.TimestampOrdering) 29 | ), 30 | Rule.ifSet(rules.getGtNow)( 31 | Rule 32 | .basic( 33 | ComparativeRulesGen.CV + ".greaterThan", 34 | s"$TV.currentTimestamp()" 35 | ) 36 | .withImport(ComparativeRulesGen.TimestampOrdering) 37 | ), 38 | rules.within.map { d => 39 | Rule.java( 40 | "io.envoyproxy.pgv.TimestampValidation.within", 41 | Seq( 42 | s"io.envoyproxy.pgv.TimestampValidation.toDuration(${d.seconds}, ${d.nanos})", 43 | "io.envoyproxy.pgv.TimestampValidation.currentTimestamp()" 44 | ), 45 | FunctionApplication( 46 | "com.google.protobuf.timestamp.Timestamp.toJavaProto" 47 | ) 48 | ) 49 | } 50 | ).flatten 51 | 52 | } 53 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/gen.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import protocbridge.Artifact 4 | import scalapb.GeneratorOption 5 | import protocbridge.SandboxedJvmGenerator 6 | 7 | object gen { 8 | def apply( 9 | options: GeneratorOption* 10 | ): (SandboxedJvmGenerator, Seq[String]) = 11 | ( 12 | SandboxedJvmGenerator.forModule( 13 | "scala", 14 | Artifact( 15 | "com.thesamet.scalapb", 16 | "scalapb-validate-codegen_2.12", 17 | scalapb.validate.compiler.BuildInfo.version 18 | ), 19 | "scalapb.validate.compiler.CodeGenerator$", 20 | scalapb.validate.compiler.CodeGenerator.suggestedDependencies 21 | ), 22 | options.map(_.toString) 23 | ) 24 | 25 | def apply( 26 | options: Set[GeneratorOption] = Set.empty 27 | ): (SandboxedJvmGenerator, Seq[String]) = apply(options.toSeq: _*) 28 | } 29 | -------------------------------------------------------------------------------- /code-gen/src/main/scala/scalapb/validate/preprocessor.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import protocbridge.Artifact 4 | import scalapb.GeneratorOption 5 | import protocbridge.SandboxedJvmGenerator 6 | 7 | object preprocessor { 8 | def apply( 9 | options: GeneratorOption* 10 | ): (SandboxedJvmGenerator, Seq[String]) = 11 | ( 12 | SandboxedJvmGenerator.forModule( 13 | "scala", 14 | Artifact( 15 | "com.thesamet.scalapb", 16 | "scalapb-validate-codegen_2.12", 17 | scalapb.validate.compiler.BuildInfo.version 18 | ), 19 | "scalapb.validate.compiler.ValidatePreprocessor$", 20 | scalapb.validate.compiler.ValidatePreprocessor.suggestedDependencies 21 | ), 22 | options.map(_.toString) 23 | ) 24 | 25 | def apply( 26 | options: Set[GeneratorOption] = Set.empty 27 | ): (SandboxedJvmGenerator, Seq[String]) = apply(options.toSeq: _*) 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/protobuf/scalapb/validate-options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package scalapb.validate; 3 | 4 | import "scalapb/scalapb.proto"; 5 | option (scalapb.options) = { 6 | scope: PACKAGE 7 | flat_package: false 8 | lenses: true 9 | java_conversions: true 10 | preserve_unknown_fields: true 11 | }; 12 | -------------------------------------------------------------------------------- /core/src/main/protobuf/scalapb/validate.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package scalapb.validate; 4 | 5 | import "scalapb/scalapb.proto"; 6 | 7 | // Field number 1089 has been assigned to ScalaPB-validate in 8 | // https://github.com/protocolbuffers/protobuf/pull/8111 9 | // 10 | // We extend ScalaPB's options, instead of google.protobuf so we can piggyback 11 | // on ScalaPB features such as package-scoped options and auxiliary options. 12 | extend scalapb.ScalaPbOptions { 13 | optional FileValidationOptions file = 1089; 14 | } 15 | 16 | extend scalapb.MessageOptions { 17 | optional MessageValidationOptions message = 1089; 18 | } 19 | 20 | extend scalapb.FieldOptions { 21 | optional FieldValidationOptions field = 1089; 22 | } 23 | 24 | message FileValidationOptions { 25 | optional bool insert_validator_instance = 1 [default=true]; 26 | optional bool validate_at_construction = 2 [default=false]; 27 | 28 | // The following options inject field transformations in the proto they are defined on. 29 | // In order to take effect, scalapb-validate's preprocessor need to be enabled in the same 30 | // file where these options are set. Example: 31 | // 32 | // import "scalapb/validate.proto"; 33 | // import "scalapb/scalapb.proto"; 34 | // 35 | // option (scalapb.options) = { 36 | // scope : PACKAGE // makes the FieldTransformations available for the entire package 37 | // preprocessors : [ "scalapb-validate-preprocessor" ] 38 | // [scalapb.validate.file] { 39 | // cats_transforms : true 40 | // unique_to_set : true 41 | // } 42 | // } 43 | optional bool unique_to_set = 3; // transform "repeated.unique: true" to sets. 44 | optional bool cats_transforms = 4; // transform to non-empty map, list and set. 45 | 46 | // Set `skip` to true to avoid generating validators for this file. This 47 | // option is also useful to be set a package-scoped option for third-party 48 | // protos that do not come with validator classes and your messages 49 | // reference them. 50 | optional bool skip = 5; 51 | 52 | // List of packages that are known to not have validators generated for 53 | // them. Same effect as setting `skip` to true for all files in the package. 54 | // This is useful when package-scoped options for that third-party package are already 55 | // defined. 56 | repeated string skip_packages = 6; 57 | } 58 | 59 | // Falls back on file validation options (where default exists) 60 | message MessageValidationOptions { 61 | optional bool insert_validator_instance = 1; 62 | optional bool validate_at_construction = 2; 63 | } 64 | 65 | message FieldValidationOptions { 66 | optional bool skip_unique_check = 1; // not implemented yet 67 | } 68 | 69 | // When field transformations are enabled, ScalaPB will attempt to find validate's extension. 70 | // It searches in the file's imports recursively. To make sure `validate/validate.proto` is 71 | // found we include it here. The Dummy message below avoids an "unused import" warning. 72 | import "validate/validate.proto"; 73 | message Dummy { 74 | optional .validate.FieldRules dummy = 1; 75 | } 76 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/ComparativeValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import math.Ordering.Implicits._ 4 | import com.google.protobuf.timestamp.Timestamp 5 | import com.google.protobuf.duration.Duration 6 | 7 | object ComparativeValidation { 8 | implicit val timestampOrdering: Ordering[Timestamp] = 9 | new Ordering[Timestamp] { 10 | def compare(x: Timestamp, y: Timestamp): Int = { 11 | val o1 = java.lang.Long.compare(x.seconds, y.seconds) 12 | if (o1 != 0) o1 13 | else java.lang.Integer.compare(x.nanos, y.nanos) 14 | } 15 | } 16 | 17 | implicit val durationOrdering: Ordering[Duration] = new Ordering[Duration] { 18 | def compare(x: Duration, y: Duration): Int = { 19 | val o1 = java.lang.Long.compare(x.seconds, y.seconds) 20 | if (o1 != 0) o1 21 | else java.lang.Integer.compare(x.nanos, y.nanos) 22 | } 23 | } 24 | 25 | def greaterThan[T: Ordering](name: String, v: T, limit: T) = 26 | Result( 27 | v > limit, 28 | ValidationFailure(name, v, s"$v must be greater than $limit") 29 | ) 30 | 31 | def lessThan[T](name: String, v: T, limit: T)(implicit order: Ordering[T]) = 32 | Result( 33 | v < limit, 34 | ValidationFailure(name, v, s"$v must be less than $limit") 35 | ) 36 | 37 | def greaterThanOrEqual[T: Ordering](name: String, v: T, limit: T) = 38 | Result( 39 | v >= limit, 40 | ValidationFailure( 41 | name, 42 | v, 43 | s"$v must be greater than or equal to $limit" 44 | ) 45 | ) 46 | 47 | def lessThanOrEqual[T: Ordering](name: String, v: T, limit: T) = 48 | Result( 49 | v <= limit, 50 | ValidationFailure( 51 | name, 52 | v, 53 | s"$v must be less than or equal to $limit" 54 | ) 55 | ) 56 | 57 | def rangeGteLte[T: Ordering](name: String, v: T, left: T, right: T) = 58 | Result( 59 | (v >= left) && (v <= right), 60 | ValidationFailure( 61 | name, 62 | v, 63 | s"$v must be in the range [$left, $right]" 64 | ) 65 | ) 66 | 67 | def rangeGteLt[T: Ordering](name: String, v: T, left: T, right: T) = 68 | Result( 69 | (v >= left) && (v < right), 70 | ValidationFailure( 71 | name, 72 | v, 73 | s"$v must be in the range [$left, $right)" 74 | ) 75 | ) 76 | 77 | def rangeGtLte[T: Ordering](name: String, v: T, left: T, right: T) = 78 | Result( 79 | (v > left) && (v <= right), 80 | ValidationFailure( 81 | name, 82 | v, 83 | s"$v must be in the range ($left, $right]" 84 | ) 85 | ) 86 | 87 | def rangeGtLt[T: Ordering](name: String, v: T, left: T, right: T) = 88 | Result( 89 | (v > left) && (v < right), 90 | ValidationFailure( 91 | name, 92 | v, 93 | s"$v must be in the range ($left, $right)" 94 | ) 95 | ) 96 | 97 | def rangeGteLteEx[T: Ordering](name: String, v: T, right: T, left: T) = 98 | Result( 99 | (v >= right) || (v <= left), 100 | ValidationFailure( 101 | name, 102 | v, 103 | s"$v must be outside the range ($left, $right)" 104 | ) 105 | ) 106 | 107 | def rangeGteLtEx[T: Ordering](name: String, v: T, right: T, left: T) = 108 | Result( 109 | (v >= right) || (v < left), 110 | ValidationFailure( 111 | name, 112 | v, 113 | s"$v must be outside the range [$left, $right)" 114 | ) 115 | ) 116 | 117 | def rangeGtLteEx[T: Ordering](name: String, v: T, right: T, left: T) = 118 | Result( 119 | (v > right) || (v <= left), 120 | ValidationFailure( 121 | name, 122 | v, 123 | s"$v must be outside the range ($left, $right]" 124 | ) 125 | ) 126 | 127 | def rangeGtLtEx[T: Ordering](name: String, v: T, right: T, left: T) = 128 | Result( 129 | (v > right) || (v < left), 130 | ValidationFailure( 131 | name, 132 | v, 133 | s"$v must be outside the range [$left, $right]" 134 | ) 135 | ) 136 | 137 | def constant[T](name: String, v: T, limit: T)(implicit order: Ordering[T]) = 138 | Result( 139 | v equiv limit, 140 | ValidationFailure(name, v, s"$v must be equal to $limit") 141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/EnumValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import scalapb.GeneratedEnum 4 | 5 | object EnumValidation { 6 | def definedOnly[T <: GeneratedEnum](name: String, value: T): Result = 7 | Result( 8 | !value.isUnrecognized, 9 | ValidationFailure(name, value.value, "must be defined") 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/MapValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | object MapValidation { 4 | val SPARSE_MAPS_NOT_SUPPORTED = "Sparse maps are not supported" 5 | 6 | def maxPairs[K, V](name: String, v: Map[K, V], limit: Int): Result = 7 | maxPairs(name, v.size, limit) 8 | 9 | def minPairs[K, V](name: String, v: Map[K, V], limit: Int): Result = 10 | minPairs(name, v.size, limit) 11 | 12 | def maxPairs[K, V](name: String, actualSize: Int, limit: Int): Result = 13 | Result( 14 | actualSize <= limit, 15 | ValidationFailure( 16 | name, 17 | actualSize, 18 | s"Expected map to have at most $limit pair, got $actualSize" 19 | ) 20 | ) 21 | 22 | def minPairs[K, V](name: String, actualSize: Int, limit: Int): Result = 23 | Result( 24 | actualSize >= limit, 25 | ValidationFailure( 26 | name, 27 | actualSize, 28 | s"Expected map to have at least $limit pair, got $actualSize" 29 | ) 30 | ) 31 | 32 | def notSparse[K, V](name: String, v: Map[K, V]) = 33 | Failure(ValidationFailure(name, v, SPARSE_MAPS_NOT_SUPPORTED)) 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/MembershipValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | object MembershipValidation { 4 | def in[T](name: String, v: T, values: Seq[T]) = 5 | Result( 6 | values.contains(v), 7 | ValidationFailure( 8 | name, 9 | v, 10 | s"""$v must be in ${values.mkString("[", ", ", "]")}"""" 11 | ) 12 | ) 13 | 14 | def notIn[T](name: String, v: T, values: Seq[T]) = 15 | Result( 16 | !values.contains(v), 17 | ValidationFailure( 18 | name, 19 | v, 20 | s"""$v must not be in ${values.mkString("[", ", ", "]")}"""" 21 | ) 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/RepeatedValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | object RepeatedValidation { 4 | def maxItems[T](name: String, v: Seq[T], limit: Int): Result = 5 | maxItems(name, v.size, limit) 6 | 7 | def minItems[T](name: String, v: Seq[T], limit: Int): Result = 8 | minItems(name, v.size, limit) 9 | 10 | def maxItems(name: String, actualSize: Int, limit: Int): Result = 11 | Result( 12 | actualSize <= limit, 13 | ValidationFailure( 14 | name, 15 | actualSize, 16 | s"Expected at most $limit elements, got $actualSize" 17 | ) 18 | ) 19 | 20 | def minItems(name: String, actualSize: Int, limit: Int): Result = 21 | Result( 22 | actualSize >= limit, 23 | ValidationFailure( 24 | name, 25 | actualSize, 26 | s"$actualSize must have at least $limit elements, got $actualSize" 27 | ) 28 | ) 29 | 30 | def unique[T](name: String, v: Seq[T]) = 31 | Result( 32 | v.distinct.size == v.size, 33 | ValidationFailure( 34 | name, 35 | v, 36 | s"$v must have distinct elements" 37 | ) 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/RequiredValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import scalapb.GeneratedOneof 4 | 5 | object RequiredValidation { 6 | def apply[T](name: String, value: Option[T]): Result = 7 | Result(value.nonEmpty, ValidationFailure(name, "None", "is required")) 8 | 9 | def apply[T](name: String, value: GeneratedOneof): Result = 10 | Result( 11 | value.isDefined, 12 | ValidationFailure(name, "Empty", "is required") 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/SetAdapter.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import scalapb.CollectionAdapter 4 | import collection.immutable.Set 5 | 6 | class SetAdapter[T] extends CollectionAdapter[T, Set[T]] { 7 | def foreach(coll: Set[T])(f: T => Unit) = { coll.map(f); {} } 8 | 9 | def empty: Set[T] = Set.empty[T] 10 | 11 | def newBuilder: Builder = 12 | Vector 13 | .newBuilder[T] 14 | .mapResult(vec => 15 | if (vec.distinct.size != vec.size) 16 | Left(new ValidationException("Got duplicate elements for Set")) 17 | else Right(vec.toSet) 18 | ) 19 | 20 | def concat(first: Set[T], second: Iterable[T]) = 21 | throw new ValidationException( 22 | "No empty instance available for cats.Data.List" 23 | ) 24 | 25 | def toIterator(value: Set[T]): Iterator[T] = value.iterator 26 | 27 | def size(value: Set[T]): Int = value.size 28 | } 29 | 30 | object SetAdapter { 31 | def apply[T]() = new SetAdapter[T] 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/TimestampValidation.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import com.google.protobuf.timestamp.Timestamp 4 | import com.google.protobuf.util.Timestamps 5 | 6 | object TimestampValidation { 7 | def currentTimestamp(): Timestamp = 8 | Timestamp.fromJavaProto(Timestamps.fromMillis(System.currentTimeMillis())) 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/Validator.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import io.envoyproxy.pgv 4 | import scala.util.Try 5 | import com.google.protobuf.InvalidProtocolBufferException 6 | 7 | sealed trait Result { 8 | def isSuccess: Boolean 9 | def isFailure: Boolean 10 | def toFailure: Option[Failure] 11 | 12 | def &&(other: Result): Result = 13 | (this, other) match { 14 | case (Success, Success) => Success 15 | case (Success, f: Failure) => f 16 | case (f: Failure, Success) => f 17 | case (Failure(left), Failure(right)) => Failure(left ::: right) 18 | } 19 | 20 | def ||(other: => Result): Result = 21 | (this, other) match { 22 | case (Success, _) => Success 23 | case (Failure(_), right) => right 24 | } 25 | } 26 | 27 | /** Represents a failure to validate a single field */ 28 | case class ValidationFailure(field: String, value: Any, reason: String) { 29 | override def toString: String = s"$field: $reason - Got $value" 30 | 31 | // Provide pgv.ValidaitonException methods for source code compatibility: 32 | @deprecated("Use 'field' instead.", "0.2.0") 33 | def getField(): String = field 34 | 35 | @deprecated("Use 'value' instead.", "0.2.0") 36 | def getValue(): Any = value 37 | 38 | @deprecated("Use 'reason' instead.", "0.2.0") 39 | def getReason(): String = reason 40 | } 41 | 42 | object Result { 43 | def run[T](code: => T): Result = 44 | Try(code) match { 45 | case scala.util.Success(_) => Success 46 | case scala.util.Failure(e: pgv.ValidationException) => 47 | Failure( 48 | new ValidationFailure( 49 | e.getField(), 50 | e.getValue(), 51 | e.getReason() 52 | ) :: Nil 53 | ) 54 | case scala.util.Failure(ex) => 55 | throw new RuntimeException( 56 | s"Unexpected exception. Please report this as a bug: ${ex.getMessage()}", 57 | ex 58 | ) 59 | } 60 | 61 | def apply(cond: => Boolean, onError: => ValidationFailure): Result = 62 | if (cond) Success else Failure(onError :: Nil) 63 | 64 | def optional[T](value: Option[T])(eval: T => Result): Result = 65 | value.fold[Result](Success)(eval) 66 | 67 | def collect(results: Iterator[Result]): Result = 68 | results.foldLeft[Result](Success) { case (left, right) => left && right } 69 | 70 | def collect(results: Iterable[Result]): Result = collect(results.iterator) 71 | 72 | def repeated[T](value: Iterator[T])(eval: T => Result): Result = 73 | collect(value.map(eval)) 74 | 75 | def repeated[T](value: Iterable[T])(eval: T => Result): Result = 76 | repeated(value.iterator)(eval) 77 | } 78 | 79 | case object Success extends Result { 80 | def isSuccess: Boolean = true 81 | def isFailure: Boolean = false 82 | def toFailure: Option[Failure] = None 83 | } 84 | 85 | case class Failure(violations: List[ValidationFailure]) extends Result { 86 | def isSuccess: Boolean = false 87 | def isFailure: Boolean = true 88 | def toFailure: Option[Failure] = Some(this) 89 | } 90 | 91 | object Failure { 92 | def apply(violation: ValidationFailure): Failure = Failure(violation :: Nil) 93 | } 94 | 95 | trait Validator[T] extends Serializable { 96 | self => 97 | def validate(t: T): Result 98 | 99 | def optional: Validator[Option[T]] = 100 | new Validator[Option[T]] { 101 | def validate(t: Option[T]): Result = 102 | t.fold[Result](Success)(self.validate) 103 | } 104 | } 105 | 106 | class ValidationException(message: String) 107 | extends InvalidProtocolBufferException(message) 108 | 109 | class FieldValidationException(failure: Failure) 110 | extends ValidationException( 111 | "Validation failed: " + failure.violations 112 | .map(_.toString()) 113 | .mkString(", ") 114 | ) {} 115 | 116 | object Validator { 117 | def apply[T: Validator] = implicitly[Validator[T]] 118 | 119 | def assertValid[T: Validator](instance: T): Unit = 120 | Validator[T].validate(instance) match { 121 | case Success => 122 | case failure: Failure => 123 | throw new FieldValidationException(failure) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /core/src/main/scala/scalapb/validate/WellKnownRegex.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import com.google.re2j.Pattern 4 | 5 | object WellKnownRegex { 6 | val HTTP_HEADER_NAME = Pattern.compile("^:?[0-9a-zA-Z!#$%&'*+-.^_|~`]+$") 7 | val HTTP_HEADER_VALUE = 8 | Pattern.compile("^[^\u0000-\u0008\u000A-\u001F\u007F]*$") 9 | val HEADER_STRING = 10 | Pattern.compile("^[^\u0000\u000A\u000D]*$") // For non-strict validation. 11 | } 12 | -------------------------------------------------------------------------------- /core/src/test/scala/scalapb/validate/ResultSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | class ResultSpec extends munit.FunSuite { 4 | 5 | test("Success is success") { 6 | assertEquals(Success.isSuccess, true) 7 | assertEquals(Success.isFailure, false) 8 | assertEquals(Success.toFailure, None) 9 | } 10 | 11 | test("Failure is failure") { 12 | val failure = Failure(ValidationFailure("field", 42, "reason")) 13 | assertEquals(failure.isSuccess, false) 14 | assertEquals(failure.isFailure, true) 15 | assertEquals(failure.toFailure, Some(failure)) 16 | } 17 | 18 | test("Result && Result") { 19 | val failure = Failure(ValidationFailure("field", 42, "reason")) 20 | val otherFailure = Failure(ValidationFailure("field2", 24, "reason")) 21 | assertEquals(Success && Success, Success) 22 | assertEquals(Success && failure, failure) 23 | assertEquals(failure && Success, failure) 24 | assertEquals( 25 | failure && otherFailure, 26 | Failure(failure.violations ::: otherFailure.violations) 27 | ) 28 | assertEquals( 29 | otherFailure && failure, 30 | Failure(otherFailure.violations ::: failure.violations) 31 | ) 32 | } 33 | 34 | test("Result || Result") { 35 | val failure = Failure(ValidationFailure("field", 42, "reason")) 36 | val otherFailure = Failure(ValidationFailure("field2", 24, "reason")) 37 | assertEquals(Success || Success, Success) 38 | assertEquals(Success || failure, Success) 39 | assertEquals(failure || Success, Success) 40 | assertEquals( 41 | failure || otherFailure, 42 | otherFailure 43 | ) 44 | } 45 | 46 | test("Result.apply") { 47 | val exception = ValidationFailure("field", 42, "reason") 48 | assertEquals( 49 | Result(true, sys.error("failure branch should be lazy")), 50 | Success 51 | ) 52 | assertEquals(Result(false, exception), Failure(exception)) 53 | } 54 | 55 | test("Result.optional") { 56 | val exception = ValidationFailure("field", 42, "reason") 57 | assertEquals( 58 | Result.optional(Some("value")) { value => 59 | assertEquals(value, "value") 60 | Success 61 | }, 62 | Success 63 | ) 64 | assertEquals( 65 | Result.optional(Some("value")) { value => 66 | assertEquals(value, "value") 67 | Failure(exception) 68 | }, 69 | Failure(exception) 70 | ) 71 | assertEquals( 72 | Result.optional[String](None) { _ => 73 | sys.error("failure branch should be lazy") 74 | }, 75 | Success 76 | ) 77 | } 78 | 79 | test("Result.collect") { 80 | val failure = Failure(ValidationFailure("field", 42, "reason")) 81 | val otherFailure = Failure(ValidationFailure("field2", 24, "reason")) 82 | assertEquals(Result.collect(Nil), Success) 83 | assertEquals(Result.collect(Success :: Nil), Success) 84 | assertEquals(Result.collect(Success :: Success :: Nil), Success) 85 | assertEquals(Result.collect(Success :: failure :: Nil), failure) 86 | assertEquals( 87 | Result.collect(Success :: failure :: otherFailure :: Nil), 88 | failure && otherFailure 89 | ) 90 | assertEquals( 91 | Result.collect(failure :: otherFailure :: Success :: Nil), 92 | failure && otherFailure 93 | ) 94 | assertEquals( 95 | Result.collect(otherFailure :: failure :: Success :: Nil), 96 | otherFailure && failure 97 | ) 98 | } 99 | 100 | test("Result.repeated") { 101 | val failure = Failure(ValidationFailure("field", 42, "reason")) 102 | val otherFailure = Failure(ValidationFailure("field2", 24, "reason")) 103 | assertEquals( 104 | Result.repeated(Nil)(_ => sys.error("cannot be invoked")), 105 | Success 106 | ) 107 | assertEquals(Result.repeated(Success :: Nil)(identity), Success) 108 | assertEquals(Result.repeated(Success :: Success :: Nil)(identity), Success) 109 | assertEquals(Result.repeated(Success :: failure :: Nil)(identity), failure) 110 | assertEquals( 111 | Result.repeated(Success :: failure :: otherFailure :: Nil)(identity), 112 | failure && otherFailure 113 | ) 114 | assertEquals( 115 | Result.repeated(failure :: otherFailure :: Success :: Nil)(identity), 116 | failure && otherFailure 117 | ) 118 | assertEquals( 119 | Result.repeated(otherFailure :: failure :: Success :: Nil)(identity), 120 | otherFailure && failure 121 | ) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf-scala2/transforms/refined/refined.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | 4 | package scalapb.transforms.refined; 5 | 6 | import "validate/validate.proto"; 7 | import "scalapb/scalapb.proto"; 8 | 9 | option (scalapb.options) = { 10 | preprocessors : [ "scalapb-validate-preprocessor" ] 11 | import : "eu.timepit.refined.api.Refined" 12 | import : "eu.timepit.refined.numeric._" 13 | import : "eu.timepit.refined.generic._" 14 | import : "shapeless.{Witness => W}" 15 | 16 | field_transformations : [ { 17 | when : {options: {[validate.rules] { int32 : {gt : 1}}}} 18 | set : {[scalapb.field] {type : "Int Refined Greater[W.`$(options.[validate.rules].int32.gt)`.T]"}} 19 | match_type : PRESENCE 20 | }, 21 | { 22 | when : {options: {[validate.rules] { int32 : {const : 1}}}} 23 | set : {[scalapb.field] {type : "Int Refined Equal[W.`$(options.[validate.rules].int32.const)`.T]"}} 24 | match_type : PRESENCE 25 | }, 26 | { 27 | when : {options: {[validate.rules] { double : {gt : 1, lte: 1}}}} 28 | set : {[scalapb.field] {type : "Double Refined Interval.OpenClosed[W.`$(options.[validate.rules].double.gt)`.T, W.`$(options.[validate.rules].double.lte)`.T]"}} 29 | match_type : PRESENCE 30 | } ] 31 | }; 32 | 33 | message RefinedTest { 34 | int32 gt5 = 1 [ (.validate.rules).int32 = {gt : 5} ]; 35 | int32 constant = 2 [ (.validate.rules).int32 = {const : 17} ]; 36 | double oc = 3 [ (.validate.rules).double = {gt : 0, lte: 100} ]; 37 | } 38 | 39 | message FreeMessage { 40 | int32 gt5 = 1; 41 | int32 constant = 2; 42 | double oc = 3; 43 | } 44 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/excluded.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.cats; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | ignore_all_transformations: true 10 | }; 11 | message Excluded { 12 | repeated string non_empty_set = 2 [(validate.rules).repeated = { min_items: 1, unique: true }]; 13 | } 14 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/non_empty_chain.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.cats; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | scope: FILE 10 | field_transformations: [ 11 | { 12 | when: { 13 | options { 14 | [validate.rules] { 15 | repeated: {min_items: 1} 16 | } 17 | } 18 | } 19 | set: { 20 | [scalapb.field] { 21 | collection: { 22 | type: "_root_.cats.data.NonEmptyChain" 23 | adapter: "_root_.scalapb.validate.cats.NonEmptyChainAdapter" 24 | non_empty: true 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | }; 31 | 32 | message NonEmptyChainTest { 33 | repeated bool values = 1 [(validate.rules).repeated = { min_items: 1 }]; 34 | } 35 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/non_empty_seq.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.cats; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | scope: FILE 10 | field_transformations: [ 11 | { 12 | when: { 13 | options { 14 | [validate.rules] { 15 | repeated: {min_items: 1} 16 | } 17 | } 18 | } 19 | set: { 20 | [scalapb.field] { 21 | collection: { 22 | type: "_root_.cats.data.NonEmptySeq" 23 | adapter: "_root_.scalapb.validate.cats.NonEmptySeqAdapter" 24 | non_empty: true 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | }; 31 | 32 | message NonEmptySeqTest { 33 | repeated int32 values = 1 [(validate.rules).repeated = { min_items: 1 }]; 34 | } 35 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/non_empty_vector.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.cats; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | scope: FILE 10 | field_transformations: [ 11 | { 12 | when: { 13 | options { 14 | [validate.rules] { 15 | repeated: {min_items: 1} 16 | } 17 | } 18 | } 19 | set: { 20 | [scalapb.field] { 21 | collection: { 22 | type: "_root_.cats.data.NonEmptyVector" 23 | adapter: "_root_.scalapb.validate.cats.NonEmptyVectorAdapter" 24 | non_empty: true 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | }; 31 | 32 | message NonEmptyVectorTest { 33 | repeated string values = 1 [(validate.rules).repeated = { min_items: 1 }]; 34 | } 35 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/novalidate.proto: -------------------------------------------------------------------------------- 1 | // this file without validate/validate.proto should compile 2 | syntax = "proto3"; 3 | 4 | package e2e.cats; 5 | 6 | message Foo { 7 | int32 bar = 1; 8 | } 9 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.cats; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "scalapb/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | scope : PACKAGE 10 | preprocessors : [ "scalapb-validate-preprocessor" ] 11 | [scalapb.validate.file]{cats_transforms : true unique_to_set : true} 12 | }; -------------------------------------------------------------------------------- /e2e/src/main/protobuf/cats/types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e.cats; 4 | 5 | import "scalapb/validate.proto"; 6 | import "validate/validate.proto"; 7 | import "scalapb/scalapb.proto"; 8 | 9 | enum Color { 10 | DEFAULT = 0; 11 | RED = 1; 12 | GREEN = 2; 13 | BLUE = 3; 14 | } 15 | 16 | message SubMsg { 17 | int32 a = 1; 18 | } 19 | 20 | message NonEmptyTypes { 21 | option (scalapb.message) = { 22 | [scalapb.validate.message] { 23 | validate_at_construction: true 24 | } 25 | }; 26 | repeated string set = 1 [(validate.rules).repeated = { unique: true }]; 27 | repeated string non_empty_set = 2 [(validate.rules).repeated = { min_items: 1, unique: true }]; 28 | repeated string non_empty_list = 3 [(validate.rules).repeated = { min_items: 1 }]; 29 | map non_empty_map = 4 [(validate.rules).map = { min_pairs: 1 }]; 30 | string foo = 5 [(validate.rules).string = {max_len: 3 }]; 31 | } 32 | 33 | message NonEmptyTypesWithSubRules { 34 | repeated string non_empty_set = 1 [ 35 | (validate.rules).repeated = { min_items: 1, unique: true, items: {string: {len: 5}}} 36 | ]; 37 | repeated string non_empty_list = 2 [ 38 | (validate.rules).repeated = { min_items: 1, items: {string: {len: 5}} }]; 39 | map non_empty_map = 3 [ 40 | (validate.rules).map = { min_pairs: 1, keys: {int32: {gte: 4}}, values: {int32: {gte: 5}}} 41 | ]; 42 | } 43 | 44 | // This message is matching NonEmptyTypes, but allows us to create invalid instances such as sets 45 | // with duplicate elements. 46 | message NonEmptyTypesTesting { 47 | repeated string set = 1; 48 | repeated string non_empty_set = 2; 49 | repeated string non_empty_list = 3; 50 | map non_empty_map = 4; 51 | string foo = 5; 52 | } -------------------------------------------------------------------------------- /e2e/src/main/protobuf/example.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package examplepb; 4 | 5 | import "validate/validate.proto"; 6 | 7 | message Person { 8 | uint64 id = 1 [(validate.rules).uint64.gt = 999]; 9 | 10 | string email = 2 [(validate.rules).string.email = true]; 11 | 12 | string name = 3 [(validate.rules).string = { 13 | pattern: "^[A-Za-z]+( [A-Za-z]+)*$", 14 | max_bytes: 256, 15 | }]; 16 | 17 | Location home = 4 [(validate.rules).message.required = true]; 18 | 19 | message Location { 20 | double lat = 1 [(validate.rules).double = { gte: -90, lte: 90 }]; 21 | double lng = 2 [(validate.rules).double = { gte: -180, lte: 180 }]; 22 | } 23 | 24 | bool is_old = 5 [(validate.rules).bool.const = false]; 25 | 26 | fixed32 age = 6 [(validate.rules).fixed32 = {gte:30, lt: 40}]; 27 | } 28 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/oneofs.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package scalapb.test; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | import: "scalapb.test.tm" 10 | }; 11 | 12 | message TestMessage { 13 | TestBigDecimal foo = 1 [(scalapb.field).type = "_root_.scala.math.BigDecimal"]; 14 | oneof value { 15 | TestBigDecimal bar = 2 [(scalapb.field).type = "_root_.scala.math.BigDecimal"]; 16 | } 17 | } 18 | 19 | message TestBigDecimal {} 20 | 21 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/optional.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package examplepb3; 4 | 5 | import "validate/validate.proto"; 6 | 7 | message Person { 8 | optional string name = 1 [(validate.rules).message.required = true]; 9 | optional int32 age = 2; 10 | optional int32 height = 3; 11 | } 12 | 13 | message RequiredMessage { 14 | optional Person person = 1 [(validate.rules).message.required = true]; 15 | } 16 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package examplepb; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "scalapb/validate.proto"; 7 | import "validate/validate.proto"; 8 | 9 | message NoValidator { 10 | option (scalapb.message) = { 11 | [scalapb.validate.message] { 12 | insert_validator_instance: false 13 | } 14 | }; 15 | uint64 id = 1 [(validate.rules).uint64.gt = 999]; 16 | } 17 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/required.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package examplepb2; 4 | 5 | import "validate/validate.proto"; 6 | 7 | message Person { 8 | message Location { 9 | required double lat = 1 [(validate.rules).double = { gte: -90, lte: 90 }]; 10 | required double lng = 2 [(validate.rules).double = { gte: -180, lte: 180 }]; 11 | } 12 | 13 | required string email = 1 [(validate.rules).string.email = true]; 14 | required Location home = 2; 15 | optional Location second_home = 3; 16 | } 17 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/skip/ours.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ours; 4 | 5 | import "scalapb/validate.proto"; 6 | import "skip/third_party.proto"; 7 | 8 | message OurMessage { 9 | third_party.CantChangeThisMessage msg_ref = 1; 10 | third_party.CantChangeThisEnum enum_ref = 2; 11 | }; 12 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/skip/third_party.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package third_party; 4 | 5 | message CantChangeThisMessage { 6 | int32 a = 1; 7 | } 8 | 9 | enum CantChangeThisEnum { 10 | DEFAULT = 0; 11 | VALUE_A = 1; 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/skip/third_party_options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package third_party; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "scalapb/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | scope : PACKAGE 10 | [scalapb.validate.file]{ 11 | skip: true 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/generation/multi_file_java_test/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_cc//cc:defs.bzl", "cc_binary") 2 | load("@rules_proto//proto:defs.bzl", "proto_library") 3 | load( 4 | "//bazel:pgv_proto_library.bzl", 5 | "pgv_cc_proto_library", 6 | ) 7 | 8 | # gazelle:go_generate_proto false 9 | 10 | proto_library( 11 | name = "test_proto", 12 | srcs = ["test.proto"], 13 | visibility = ["//visibility:public"], 14 | deps = [ 15 | "@com_google_protobuf//:any_proto", 16 | ], 17 | ) 18 | 19 | pgv_cc_proto_library( 20 | name = "test_cc_proto", 21 | visibility = ["//visibility:public"], 22 | deps = [":test_proto"], 23 | ) 24 | 25 | cc_binary( 26 | name = "cc-mfjt-test", 27 | srcs = ["main.cc"], 28 | visibility = ["//visibility:public"], 29 | deps = [ 30 | ":test_cc_proto", 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/generation/multi_file_java_test/README.md: -------------------------------------------------------------------------------- 1 | # Multi File Java Test # 2 | 3 | Test that a file with: `option java_multiple_files = true;` not generated in 4 | java can properly be generated without error. Due to `java` not needing to be 5 | in the generation path at all we had to split it out into it's own directory. 6 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/generation/multi_file_java_test/main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "tests/generation/multi_file_java_test/test.pb.h" 4 | #include "tests/generation/multi_file_java_test/test.pb.validate.h" 5 | 6 | int main(int argc, char** argv) { 7 | return 0; 8 | } 9 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/generation/multi_file_java_test/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.mfjt; 4 | option go_package = "harness"; 5 | option java_multiple_files = true; 6 | 7 | import "google/protobuf/any.proto"; 8 | 9 | message Message { 10 | google.protobuf.Any data = 1; 11 | } 12 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/envoyproxy/protoc-gen-validate/tests 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/envoyproxy/protoc-gen-validate v0.6.1 7 | golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d 8 | google.golang.org/protobuf v1.27.1 9 | ) 10 | 11 | replace github.com/envoyproxy/protoc-gen-validate => ../ 12 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 3 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 4 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 5 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 7 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 8 | github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= 9 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 10 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= 13 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 16 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 20 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 21 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 22 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 23 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 24 | golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 25 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 26 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 27 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 28 | golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= 29 | golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 30 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 40 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 41 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 42 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 43 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 44 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 46 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 47 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 48 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 49 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 52 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 54 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 55 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 56 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | load("@rules_cc//cc:defs.bzl", "cc_proto_library") 3 | load("@rules_java//java:defs.bzl", "java_proto_library") 4 | load("@rules_proto//proto:defs.bzl", "proto_library") 5 | load("@com_google_protobuf//:protobuf.bzl", "py_proto_library") 6 | load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") 7 | 8 | proto_library( 9 | name = "harness_proto", 10 | srcs = ["harness.proto"], 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "@com_google_protobuf//:any_proto", 14 | ], 15 | ) 16 | 17 | go_proto_library( 18 | name = "harness_go_proto", 19 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/go", 20 | proto = ":harness_proto", 21 | visibility = ["//visibility:public"], 22 | ) 23 | 24 | cc_proto_library( 25 | name = "harness_cc_proto", 26 | visibility = ["//visibility:public"], 27 | deps = [ 28 | ":harness_proto", 29 | ], 30 | ) 31 | 32 | java_proto_library( 33 | name = "harness_java_proto", 34 | visibility = ["//visibility:public"], 35 | deps = [":harness_proto"], 36 | ) 37 | 38 | go_library( 39 | name = "go_default_library", 40 | embed = [":harness_go_proto"], 41 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness", 42 | visibility = ["//visibility:public"], 43 | ) 44 | 45 | py_proto_library( 46 | name = "harness_py_proto", 47 | srcs = ["harness.proto"], 48 | visibility = ["//visibility:public"], 49 | deps = ["@com_google_protobuf//:protobuf_python"], 50 | ) 51 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/.gitignore: -------------------------------------------------------------------------------- 1 | *.pb.cc 2 | *.pb.h 3 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_proto_library") 2 | load("@rules_proto//proto:defs.bzl", "proto_library") 3 | load("@com_google_protobuf//:protobuf.bzl", "py_proto_library") 4 | load( 5 | "//bazel:pgv_proto_library.bzl", 6 | "pgv_cc_proto_library", 7 | "pgv_go_proto_library", 8 | "pgv_java_proto_library", 9 | ) 10 | 11 | # gazelle:go_generate_proto false 12 | 13 | proto_library( 14 | name = "cases_proto", 15 | srcs = [ 16 | "bool.proto", 17 | "bytes.proto", 18 | "enums.proto", 19 | "filename-with-dash.proto", 20 | "kitchen_sink.proto", 21 | "maps.proto", 22 | "messages.proto", 23 | "numbers.proto", 24 | "oneofs.proto", 25 | "repeated.proto", 26 | "strings.proto", 27 | "subdirectory/in_subdirectory.proto", 28 | "wkt_any.proto", 29 | "wkt_duration.proto", 30 | "wkt_nested.proto", 31 | "wkt_timestamp.proto", 32 | "wkt_wrappers.proto", 33 | ], 34 | visibility = ["//visibility:public"], 35 | deps = [ 36 | "//tests/harness/cases/other_package:embed_proto", 37 | "//tests/harness/cases/yet_another_package:embed_proto", 38 | "//validate:validate_proto", 39 | "@com_google_protobuf//:any_proto", 40 | "@com_google_protobuf//:duration_proto", 41 | "@com_google_protobuf//:timestamp_proto", 42 | "@com_google_protobuf//:wrappers_proto", 43 | ], 44 | ) 45 | 46 | pgv_go_proto_library( 47 | name = "go", 48 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go", 49 | proto = ":cases_proto", 50 | deps = [ 51 | "//tests/harness/cases/other_package:go", 52 | "//tests/harness/cases/yet_another_package:go", 53 | "@org_golang_google_protobuf//types/known/anypb:go_default_library", 54 | "@org_golang_google_protobuf//types/known/durationpb:go_default_library", 55 | "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", 56 | "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library", 57 | ], 58 | ) 59 | 60 | pgv_cc_proto_library( 61 | name = "cc", 62 | cc_deps = [ 63 | "//tests/harness/cases/other_package:cc", 64 | "//tests/harness/cases/yet_another_package:cc", 65 | ], 66 | visibility = ["//tests:__subpackages__"], 67 | deps = [":cases_proto"], 68 | ) 69 | 70 | java_proto_library( 71 | name = "cases_java_proto", 72 | visibility = ["//visibility:public"], 73 | deps = [":cases_proto"], 74 | ) 75 | 76 | pgv_java_proto_library( 77 | name = "java", 78 | java_deps = [ 79 | ":cases_java_proto", 80 | "//tests/harness/cases/other_package:java", 81 | "//tests/harness/cases/yet_another_package:java", 82 | ], 83 | visibility = ["//visibility:public"], 84 | deps = [":cases_proto"], 85 | ) 86 | 87 | # There is not currently a canonical implementation of py_proto_library in Bazel. 88 | # This py_proto_library implementation is from "github.com/protocolbuffers/protobuf" and works differently from other 89 | # languages' canonical implementations - for example, it doesn't take "proto_library" targets as input. 90 | py_proto_library( 91 | name = "cases_py_proto", 92 | srcs = [ 93 | "bool.proto", 94 | "bytes.proto", 95 | "enums.proto", 96 | "kitchen_sink.proto", 97 | "maps.proto", 98 | "messages.proto", 99 | "numbers.proto", 100 | "oneofs.proto", 101 | "repeated.proto", 102 | "strings.proto", 103 | "wkt_any.proto", 104 | "wkt_duration.proto", 105 | "wkt_nested.proto", 106 | "wkt_timestamp.proto", 107 | "wkt_wrappers.proto", 108 | ], 109 | visibility = ["//visibility:public"], 110 | deps = [ 111 | "//validate:validate_py", 112 | "//tests/harness/cases/other_package:embed_python_proto", 113 | "//tests/harness/cases/yet_another_package:embed_python_proto", 114 | "@com_google_protobuf//:protobuf_python", 115 | ], 116 | ) 117 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/bool.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | message BoolNone { bool val = 1; } 8 | message BoolConstTrue { bool val = 1 [(validate.rules).bool.const = true]; } 9 | message BoolConstFalse { bool val = 1 [(validate.rules).bool.const = false]; } 10 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/bytes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | message BytesNone { bytes val = 1; } 8 | message BytesConst { bytes val = 1 [(validate.rules).bytes.const = "foo"]; } 9 | message BytesIn { bytes val = 1 [(validate.rules).bytes = {in: ["bar", "baz"]}]; } 10 | message BytesNotIn { bytes val = 1 [(validate.rules).bytes = {not_in: ["fizz", "buzz"]}]; } 11 | message BytesLen { bytes val = 1 [(validate.rules).bytes.len = 3]; } 12 | message BytesMinLen { bytes val = 1 [(validate.rules).bytes.min_len = 3]; } 13 | message BytesMaxLen { bytes val = 1 [(validate.rules).bytes.max_len = 5]; } 14 | message BytesMinMaxLen { bytes val = 1 [(validate.rules).bytes = {min_len: 3, max_len: 5}]; } 15 | message BytesEqualMinMaxLen { bytes val = 1 [(validate.rules).bytes = {min_len: 5, max_len: 5}]; } 16 | message BytesPattern { bytes val = 1 [(validate.rules).bytes.pattern = "^[\x00-\x7F]+$"]; } 17 | message BytesPrefix { bytes val = 1 [(validate.rules).bytes.prefix = "\x99"]; } 18 | message BytesContains { bytes val = 1 [(validate.rules).bytes.contains = "bar"]; } 19 | message BytesSuffix { bytes val = 1 [(validate.rules).bytes.suffix = "buz\x7a"]; } 20 | message BytesIP { bytes val = 1 [(validate.rules).bytes.ip = true]; } 21 | message BytesIPv4 { bytes val = 1 [(validate.rules).bytes.ipv4 = true]; } 22 | message BytesIPv6 { bytes val = 1 [(validate.rules).bytes.ipv6 = true]; } 23 | message BytesIPv6Ignore { bytes val = 1 [(validate.rules).bytes = {ipv6: true, ignore_empty: true}]; } 24 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/enums.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | import "tests/harness/cases/other_package/embed.proto"; 7 | import "tests/harness/cases/yet_another_package/embed.proto"; 8 | 9 | enum TestEnum { 10 | ZERO = 0; 11 | ONE = 1; 12 | TWO = 2; 13 | } 14 | 15 | enum TestEnumAlias { 16 | option allow_alias = true; 17 | 18 | A = 0; 19 | B = 1; 20 | C = 2; 21 | 22 | ALPHA = 0; 23 | BETA = 1; 24 | GAMMA = 2; 25 | } 26 | 27 | message EnumNone { TestEnum val = 1; } 28 | 29 | message EnumConst { TestEnum val = 1 [(validate.rules).enum.const = 2];} 30 | message EnumAliasConst { TestEnumAlias val = 1 [(validate.rules).enum.const = 2];} 31 | 32 | message EnumDefined { TestEnum val = 1 [(validate.rules).enum.defined_only = true];} 33 | message EnumAliasDefined { TestEnumAlias val = 1 [(validate.rules).enum.defined_only = true];} 34 | 35 | message EnumIn { TestEnum val = 1 [(validate.rules).enum = { in: [0, 2]}];} 36 | message EnumAliasIn { TestEnumAlias val = 1 [(validate.rules).enum = { in: [0, 2]}];} 37 | 38 | message EnumNotIn { TestEnum val = 1 [(validate.rules).enum = { not_in: [1]}];} 39 | message EnumAliasNotIn { TestEnumAlias val = 1 [(validate.rules).enum = { not_in: [1]}]; } 40 | 41 | message EnumExternal { other_package.Embed.Enumerated val = 1 [(validate.rules).enum.defined_only = true]; } 42 | 43 | message RepeatedEnumDefined { repeated TestEnum val = 1 [(validate.rules).repeated.items.enum.defined_only = true]; } 44 | message RepeatedExternalEnumDefined { repeated other_package.Embed.Enumerated val = 1 [(validate.rules).repeated.items.enum.defined_only = true]; } 45 | message RepeatedYetAnotherExternalEnumDefined { repeated yet_another_package.Embed.Enumerated val = 1 [(validate.rules).repeated.items.enum.defined_only = true]; } 46 | 47 | message MapEnumDefined { map val = 1 [(validate.rules).map.values.enum.defined_only = true]; } 48 | message MapExternalEnumDefined { map val = 1 [(validate.rules).map.values.enum.defined_only = true]; } 49 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/filename-with-dash.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/kitchen_sink.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | import "google/protobuf/wrappers.proto"; 8 | import "google/protobuf/duration.proto"; 9 | import "google/protobuf/timestamp.proto"; 10 | import "google/protobuf/any.proto"; 11 | 12 | enum ComplexTestEnum { 13 | ComplexZero = 0; 14 | ComplexONE = 1; 15 | ComplexTWO = 2; 16 | } 17 | 18 | message ComplexTestMsg { 19 | string const = 1 [(validate.rules).string.const = "abcd"]; 20 | ComplexTestMsg nested = 2; 21 | int32 int_const = 3 [(validate.rules).int32.const = 5]; 22 | bool bool_const = 4 [(validate.rules).bool.const = false]; 23 | google.protobuf.FloatValue float_val = 5 [(validate.rules).float.gt = 0]; 24 | google.protobuf.Duration dur_val = 6 [(validate.rules).duration.lt = {seconds: 17}, (validate.rules).duration.required = true]; 25 | google.protobuf.Timestamp ts_val = 7 [(validate.rules).timestamp.gt = {seconds: 7}]; 26 | ComplexTestMsg another = 8; 27 | float float_const = 9 [(validate.rules).float.lt = 8]; 28 | double double_in = 10 [(validate.rules).double = {in: [456.789, 123]}]; 29 | ComplexTestEnum enum_const = 11 [(validate.rules).enum.const = 2]; 30 | google.protobuf.Any any_val = 12 [(validate.rules).any = {in: ["type.googleapis.com/google.protobuf.Duration"]}]; 31 | repeated google.protobuf.Timestamp rep_ts_val = 13 [(validate.rules).repeated = { items { timestamp { gte { nanos: 1000000}}}}]; 32 | map map_val = 14 [(validate.rules).map.keys.sint32.lt = 0]; 33 | bytes bytes_val = 15 [(validate.rules).bytes.const = "\x00\x99"]; 34 | oneof o { 35 | option (validate.required) = true; 36 | 37 | string x = 16; 38 | int32 y = 17; 39 | } 40 | } 41 | 42 | message KitchenSinkMessage { ComplexTestMsg val = 1; } 43 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/maps.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | message MapNone { map val = 1; } 8 | 9 | message MapMin { map val = 1 [(validate.rules).map.min_pairs = 2]; } 10 | message MapMax { map val = 1 [(validate.rules).map.max_pairs = 3]; } 11 | message MapMinMax { map val = 1 [(validate.rules).map = {min_pairs: 2, max_pairs: 4}]; } 12 | message MapExact { map val = 1 [(validate.rules).map = {min_pairs: 3, max_pairs: 3}]; } 13 | 14 | message MapNoSparse { 15 | map val = 1 [(validate.rules).map.no_sparse = true]; 16 | message Msg {} 17 | } 18 | 19 | message MapKeys { map val = 1 [(validate.rules).map.keys.sint64.lt = 0]; } 20 | message MapValues { map val = 1 [(validate.rules).map.values.string.min_len = 3]; } 21 | 22 | message MapKeysPattern { map val = 1 [(validate.rules).map.keys.string.pattern = "(?i)^[a-z0-9]+$"]; } 23 | message MapValuesPattern { map val = 1 [(validate.rules).map.values.string.pattern = "(?i)^[a-z0-9]+$"]; } 24 | 25 | message MapRecursive { 26 | map val = 1; 27 | message Msg { 28 | string val = 1 [(validate.rules).string.min_len = 3]; 29 | } 30 | } 31 | 32 | message MapExactIgnore { map val = 1 [(validate.rules).map = {min_pairs: 3, max_pairs: 3, ignore_empty: true}]; } 33 | 34 | message MultipleMaps { 35 | map first = 1 [(validate.rules).map.keys.uint32.gt = 0]; 36 | map second = 2 [(validate.rules).map.keys.int32.lt = 0]; 37 | map third = 3 [(validate.rules).map.keys.int32.gt = 0]; 38 | } 39 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "tests/harness/cases/other_package/embed.proto"; 6 | 7 | import "validate/validate.proto"; 8 | 9 | message TestMsg { 10 | string const = 1 [(validate.rules).string.const = "foo"]; 11 | TestMsg nested = 2; 12 | } 13 | 14 | message MessageNone { 15 | NoneMsg val = 1; 16 | message NoneMsg {} 17 | } 18 | 19 | message MessageDisabled { 20 | option (validate.disabled) = true; 21 | uint64 val = 1 [(validate.rules).uint64.gt = 123]; 22 | } 23 | 24 | message MessageIgnored { 25 | option (validate.ignored) = true; 26 | uint64 val = 1 [(validate.rules).uint64.gt = 123]; 27 | } 28 | 29 | message Message { TestMsg val = 1; } 30 | message MessageCrossPackage { tests.harness.cases.other_package.Embed val = 1; } 31 | message MessageSkip { TestMsg val = 1 [(validate.rules).message.skip = true];} 32 | message MessageRequired { TestMsg val = 1 [(validate.rules).message.required = true]; } 33 | message MessageRequiredButOptional { optional TestMsg val = 1 [(validate.rules).message.required = true]; } 34 | 35 | message MessageRequiredOneof { 36 | oneof one { 37 | option (validate.required) = true; 38 | TestMsg val = 1 [(validate.rules).message.required = true]; 39 | } 40 | } 41 | 42 | message MessageWith3dInside {} 43 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/oneofs.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | message TestOneOfMsg { 8 | bool val = 1 [(validate.rules).bool.const = true]; 9 | } 10 | 11 | message OneOfNone { 12 | oneof o { 13 | string x = 1; 14 | int32 y = 2; 15 | } 16 | } 17 | 18 | message OneOf { 19 | oneof o { 20 | string x = 1 [(validate.rules).string.prefix = "foo"]; 21 | int32 y = 2 [(validate.rules).int32.gt = 0]; 22 | TestOneOfMsg z = 3; 23 | } 24 | } 25 | 26 | message OneOfRequired { 27 | oneof o { 28 | option (validate.required) = true; 29 | 30 | string x = 1; 31 | int32 y = 2; 32 | int32 name_with_underscores = 3; 33 | int32 under_and_1_number = 4; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/other_package/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_proto_library") 2 | load("@com_google_protobuf//:protobuf.bzl", "py_proto_library") 3 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 4 | load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") 5 | load("@rules_proto//proto:defs.bzl", "proto_library") 6 | load( 7 | "//bazel:pgv_proto_library.bzl", 8 | "pgv_cc_proto_library", 9 | "pgv_go_proto_library", 10 | "pgv_java_proto_library", 11 | ) 12 | 13 | proto_library( 14 | name = "embed_proto", 15 | srcs = [ 16 | "embed.proto", 17 | ], 18 | visibility = ["//visibility:public"], 19 | deps = ["//validate:validate_proto"], 20 | ) 21 | 22 | pgv_go_proto_library( 23 | name = "go", 24 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/other_package/go", 25 | proto = ":embed_proto", 26 | deps = [ 27 | "@org_golang_google_protobuf//types/known/anypb:go_default_library", 28 | ], 29 | ) 30 | 31 | pgv_cc_proto_library( 32 | name = "cc", 33 | visibility = ["//tests:__subpackages__"], 34 | deps = [":embed_proto"], 35 | ) 36 | 37 | proto_library( 38 | name = "other_package_proto", 39 | srcs = ["embed.proto"], 40 | visibility = ["//visibility:public"], 41 | deps = ["//validate:validate_proto"], 42 | ) 43 | 44 | go_proto_library( 45 | name = "other_package_go_proto", 46 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/other_package", 47 | proto = ":other_package_proto", 48 | visibility = ["//visibility:public"], 49 | deps = ["//validate:go_default_library"], 50 | ) 51 | 52 | go_library( 53 | name = "go_default_library", 54 | embed = [":other_package_go_proto"], 55 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/other_package", 56 | visibility = ["//visibility:public"], 57 | ) 58 | 59 | java_proto_library( 60 | name = "embed_java_proto", 61 | visibility = ["//visibility:public"], 62 | deps = [":embed_proto"], 63 | ) 64 | 65 | pgv_java_proto_library( 66 | name = "java", 67 | java_deps = [":embed_java_proto"], 68 | visibility = ["//visibility:public"], 69 | deps = [":embed_proto"], 70 | ) 71 | 72 | py_proto_library( 73 | name = "embed_python_proto", 74 | srcs = ["embed.proto"], 75 | visibility = ["//visibility:public"], 76 | deps = [ 77 | "//validate:validate_py", 78 | "@com_google_protobuf//:protobuf_python", 79 | ], 80 | ) 81 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/other_package/embed.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases.other_package; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/other_package/go;other_package"; 5 | 6 | import "validate/validate.proto"; 7 | 8 | // Validate message embedding across packages. 9 | message Embed { 10 | int64 val = 1 [(validate.rules).int64.gt = 0]; 11 | 12 | enum Enumerated { VALUE = 0; } 13 | } 14 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/repeated.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "tests/harness/cases/other_package/embed.proto"; 6 | 7 | import "validate/validate.proto"; 8 | import "google/protobuf/duration.proto"; 9 | 10 | message Embed { int64 val = 1 [(validate.rules).int64.gt = 0]; } 11 | enum AnEnum { 12 | X = 0; 13 | Y = 1; 14 | } 15 | 16 | message RepeatedNone { repeated int64 val = 1; } 17 | message RepeatedEmbedNone { repeated Embed val = 1; } 18 | message RepeatedEmbedCrossPackageNone { repeated tests.harness.cases.other_package.Embed val = 1; } 19 | message RepeatedMin { repeated Embed val = 1 [(validate.rules).repeated.min_items = 2]; } 20 | message RepeatedMax { repeated double val = 1 [(validate.rules).repeated.max_items = 3]; } 21 | message RepeatedMinMax { repeated sfixed32 val = 1 [(validate.rules).repeated = {min_items: 2, max_items: 4}]; } 22 | message RepeatedExact { repeated uint32 val = 1 [(validate.rules).repeated = {min_items: 3, max_items: 3}]; } 23 | message RepeatedUnique { repeated string val = 1 [(validate.rules).repeated.unique = true]; } 24 | message RepeatedItemRule { repeated float val = 1 [(validate.rules).repeated.items.float.gt = 0]; } 25 | message RepeatedItemPattern { repeated string val = 1 [(validate.rules).repeated.items.string.pattern = "(?i)^[a-z0-9]+$"]; } 26 | message RepeatedEmbedSkip { repeated Embed val = 1 [(validate.rules).repeated.items.message.skip = true]; } 27 | message RepeatedItemIn { repeated string val = 1 [(validate.rules).repeated.items.string = {in: ["foo", "bar"]}]; } 28 | message RepeatedItemNotIn { repeated string val = 1 [(validate.rules).repeated.items.string = {not_in: ["foo", "bar"]}]; } 29 | message RepeatedEnumIn { repeated AnEnum val = 1 [(validate.rules).repeated.items.enum = {in: [0]}]; } 30 | message RepeatedEnumNotIn { repeated AnEnum val = 1 [(validate.rules).repeated.items.enum = {not_in: [0]}]; } 31 | message RepeatedEmbeddedEnumIn { repeated AnotherInEnum val = 1 [(validate.rules).repeated.items.enum = {in: [0]}]; enum AnotherInEnum {A = 0; B = 1; }} 32 | message RepeatedEmbeddedEnumNotIn { repeated AnotherNotInEnum val = 1 [(validate.rules).repeated.items.enum = {not_in: [0]}]; enum AnotherNotInEnum {A = 0; B = 1; }} 33 | message RepeatedMinAndItemLen { repeated string val = 1 [(validate.rules).repeated = { items { string { len: 3 } }, min_items: 1 }]; } 34 | message RepeatedMinAndMaxItemLen { repeated string val = 1 [(validate.rules).repeated.min_items = 1, (validate.rules).repeated.max_items = 3]; } 35 | message RepeatedDuration { repeated google.protobuf.Duration val = 1 [(validate.rules).repeated = { items { duration { gte { nanos: 1000000}}}}]; } 36 | message RepeatedExactIgnore { repeated uint32 val = 1 [(validate.rules).repeated = {min_items: 3, max_items: 3, ignore_empty: true}]; } 37 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/strings.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | message StringNone { string val = 1; } 8 | message StringConst { string val = 1 [(validate.rules).string.const = "foo"]; } 9 | message StringIn { string val = 1 [(validate.rules).string = {in: ["bar", "baz"]}]; } 10 | message StringNotIn { string val = 1 [(validate.rules).string = {not_in: ["fizz", "buzz"]}]; } 11 | message StringLen { string val = 1 [(validate.rules).string.len = 3]; } 12 | message StringMinLen { string val = 1 [(validate.rules).string.min_len = 3]; } 13 | message StringMaxLen { string val = 1 [(validate.rules).string.max_len = 5]; } 14 | message StringMinMaxLen { string val = 1 [(validate.rules).string = {min_len: 3, max_len: 5}]; } 15 | message StringEqualMinMaxLen { string val = 1 [(validate.rules).string = {min_len: 5, max_len: 5}]; } 16 | message StringLenBytes { string val = 1 [(validate.rules).string.len_bytes = 4]; } 17 | message StringMinBytes { string val = 1 [(validate.rules).string.min_bytes = 4]; } 18 | message StringMaxBytes { string val = 1 [(validate.rules).string.max_bytes = 8]; } 19 | message StringMinMaxBytes { string val = 1 [(validate.rules).string = {min_bytes: 4, max_bytes: 8}]; } 20 | message StringEqualMinMaxBytes { string val = 1 [(validate.rules).string = {min_bytes: 4, max_bytes: 8}]; } 21 | message StringPattern { string val = 1 [(validate.rules).string.pattern = "(?i)^[a-z0-9]+$"]; } 22 | message StringPatternEscapes { string val = 1 [(validate.rules).string.pattern = "\\* \\\\ \\w"]; } 23 | message StringPrefix { string val = 1 [(validate.rules).string.prefix = "foo"]; } 24 | message StringContains { string val = 1 [(validate.rules).string.contains = "bar"]; } 25 | message StringNotContains { string val = 1 [(validate.rules).string.not_contains = "bar"]; } 26 | message StringSuffix { string val = 1 [(validate.rules).string.suffix = "baz"]; } 27 | message StringEmail { string val = 1 [(validate.rules).string.email = true]; } 28 | message StringAddress { string val = 1 [(validate.rules).string.address = true]; } 29 | message StringHostname { string val = 1 [(validate.rules).string.hostname = true]; } 30 | message StringIP { string val = 1 [(validate.rules).string.ip = true]; } 31 | message StringIPv4 { string val = 1 [(validate.rules).string.ipv4 = true]; } 32 | message StringIPv6 { string val = 1 [(validate.rules).string.ipv6 = true]; } 33 | message StringURI { string val = 1 [(validate.rules).string.uri = true]; } 34 | message StringURIRef { string val = 1 [(validate.rules).string.uri_ref = true]; } 35 | message StringUUID { string val = 1 [(validate.rules).string.uuid = true]; } 36 | message StringHttpHeaderName { string val = 1 [(validate.rules).string.well_known_regex = HTTP_HEADER_NAME]; } 37 | message StringHttpHeaderValue { string val = 1 [(validate.rules).string.well_known_regex = HTTP_HEADER_VALUE]; } 38 | message StringValidHeader { string val = 1 [(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE, strict: false}]; } 39 | message StringUUIDIgnore { string val = 1 [(validate.rules).string = {uuid: true, ignore_empty: true}]; } 40 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/subdirectory/in_subdirectory.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | 7 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/wkt_any.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | import "google/protobuf/any.proto"; 7 | 8 | message AnyNone { google.protobuf.Any val = 1; } 9 | message AnyRequired { google.protobuf.Any val = 1 [(validate.rules).any.required = true]; } 10 | message AnyIn { google.protobuf.Any val = 1 [(validate.rules).any = {in: ["type.googleapis.com/google.protobuf.Duration"]}];} 11 | message AnyNotIn { google.protobuf.Any val = 1 [(validate.rules).any = {not_in: ["type.googleapis.com/google.protobuf.Timestamp"]}];} 12 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/wkt_duration.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | 6 | import "validate/validate.proto"; 7 | import "google/protobuf/duration.proto"; 8 | 9 | message DurationNone { google.protobuf.Duration val = 1; } 10 | 11 | message DurationRequired { google.protobuf.Duration val = 1 [(validate.rules).duration.required = true]; } 12 | // TODO(htuch): Add a very large duration, e.g. {seconds: 315576000000}, once 13 | // #34 is resolved. 14 | message DurationConst { google.protobuf.Duration val = 1 [(validate.rules).duration.const = {seconds: 3}]; } 15 | 16 | message DurationIn { google.protobuf.Duration val = 1 [(validate.rules).duration = {in: [{seconds:1}, {nanos:1000}]}]; } 17 | message DurationNotIn { google.protobuf.Duration val = 1 [(validate.rules).duration = {not_in: [{}]}]; } 18 | 19 | message DurationLT { google.protobuf.Duration val = 1 [(validate.rules).duration.lt = {}]; } 20 | message DurationLTE { google.protobuf.Duration val = 1 [(validate.rules).duration.lte = {seconds: 1}]; } 21 | message DurationGT { google.protobuf.Duration val = 1 [(validate.rules).duration.gt = {nanos: 1000}]; } 22 | message DurationGTE { google.protobuf.Duration val = 1 [(validate.rules).duration.gte = {nanos: 1000000}]; } 23 | message DurationGTLT { google.protobuf.Duration val = 1 [(validate.rules).duration = {gt: {}, lt: {seconds: 1}}]; } 24 | message DurationExLTGT { google.protobuf.Duration val = 1 [(validate.rules).duration = {lt: {}, gt: {seconds: 1}}]; } 25 | message DurationGTELTE { google.protobuf.Duration val = 1 [(validate.rules).duration = {gte: {seconds: 60}, lte: {seconds: 3600}}]; } 26 | message DurationExGTELTE { google.protobuf.Duration val = 1 [(validate.rules).duration = {lte: {seconds: 60}, gte: {seconds: 3600}}]; } 27 | 28 | // Regression for earlier bug where missing Duration field would short circuit 29 | // evaluation in C++. 30 | message DurationFieldWithOtherFields { 31 | google.protobuf.Duration duration_val = 1 [(validate.rules).duration.lte = {seconds: 1}]; 32 | int32 int_val = 2 [(validate.rules).int32.gt = 16]; 33 | } 34 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/wkt_nested.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | 6 | import "validate/validate.proto"; 7 | 8 | message WktLevelOne { 9 | message WktLevelTwo { 10 | message WktLevelThree { 11 | string uuid = 1 [(validate.rules).string.uuid = true]; 12 | } 13 | 14 | WktLevelThree three = 1 [(validate.rules).message.required = true]; 15 | } 16 | 17 | WktLevelTwo two = 1 [(validate.rules).message.required = true]; 18 | } -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/wkt_timestamp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | import "google/protobuf/timestamp.proto"; 7 | 8 | message TimestampNone { google.protobuf.Timestamp val = 1; } 9 | message TimestampRequired { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.required = true]; } 10 | message TimestampConst { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.const = {seconds: 3}]; } 11 | 12 | message TimestampLT { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.lt = {}]; } 13 | message TimestampLTE { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.lte = {seconds: 1}]; } 14 | message TimestampGT { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.gt = {nanos: 1000}]; } 15 | message TimestampGTE { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.gte = {nanos: 1000000}]; } 16 | message TimestampGTLT { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp = {gt: {}, lt: {seconds: 1}}]; } 17 | message TimestampExLTGT { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp = {lt: {}, gt: {seconds: 1}}]; } 18 | message TimestampGTELTE { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp = {gte: {seconds: 60}, lte: {seconds: 3600}}]; } 19 | message TimestampExGTELTE { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp = {lte: {seconds: 60}, gte: {seconds: 3600}}]; } 20 | 21 | message TimestampLTNow { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.lt_now = true]; } 22 | message TimestampGTNow { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.gt_now = true]; } 23 | 24 | message TimestampWithin { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp.within.seconds = 3600]; } 25 | 26 | message TimestampLTNowWithin { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp = {lt_now: true, within: {seconds: 3600}}]; } 27 | message TimestampGTNowWithin { google.protobuf.Timestamp val = 1 [(validate.rules).timestamp = {gt_now: true, within: {seconds: 3600}}]; } 28 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/wkt_wrappers.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go;cases"; 5 | import "validate/validate.proto"; 6 | import "google/protobuf/wrappers.proto"; 7 | 8 | message WrapperNone { google.protobuf.Int32Value val = 1; } 9 | 10 | message WrapperFloat { google.protobuf.FloatValue val = 1 [(validate.rules).float.gt = 0]; } 11 | message WrapperDouble { google.protobuf.DoubleValue val = 1 [(validate.rules).double.gt = 0]; } 12 | message WrapperInt64 { google.protobuf.Int64Value val = 1 [(validate.rules).int64.gt = 0]; } 13 | message WrapperInt32 { google.protobuf.Int32Value val = 1 [(validate.rules).int32.gt = 0]; } 14 | message WrapperUInt64 { google.protobuf.UInt64Value val = 1 [(validate.rules).uint64.gt = 0]; } 15 | message WrapperUInt32 { google.protobuf.UInt32Value val = 1 [(validate.rules).uint32.gt = 0]; } 16 | message WrapperBool { google.protobuf.BoolValue val = 1 [(validate.rules).bool.const = true]; } 17 | message WrapperString { google.protobuf.StringValue val = 1 [(validate.rules).string.suffix = "bar"]; } 18 | message WrapperBytes { google.protobuf.BytesValue val = 1 [(validate.rules).bytes.min_len = 3]; } 19 | message WrapperRequiredString { google.protobuf.StringValue val = 1 [(validate.rules).string.const = "bar", (validate.rules).message.required = true]; } 20 | message WrapperRequiredEmptyString { google.protobuf.StringValue val = 1 [(validate.rules).string.const = "", (validate.rules).message.required = true]; } 21 | message WrapperOptionalUuidString { google.protobuf.StringValue val = 1 [(validate.rules).string.uuid = true, (validate.rules).message.required = false]; } 22 | message WrapperRequiredFloat { google.protobuf.FloatValue val = 1 [(validate.rules).float.gt = 0, (validate.rules).message.required = true]; } 23 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/yet_another_package/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_proto_library") 2 | load("@com_google_protobuf//:protobuf.bzl", "py_proto_library") 3 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 4 | load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") 5 | load("@rules_proto//proto:defs.bzl", "proto_library") 6 | load( 7 | "//bazel:pgv_proto_library.bzl", 8 | "pgv_cc_proto_library", 9 | "pgv_go_proto_library", 10 | "pgv_java_proto_library", 11 | ) 12 | 13 | proto_library( 14 | name = "embed_proto", 15 | srcs = [ 16 | "embed.proto", 17 | ], 18 | visibility = ["//visibility:public"], 19 | deps = ["//validate:validate_proto"], 20 | ) 21 | 22 | pgv_go_proto_library( 23 | name = "go", 24 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/yet_another_package/go", 25 | proto = ":embed_proto", 26 | deps = [ 27 | "@org_golang_google_protobuf//types/known/anypb:go_default_library", 28 | ], 29 | ) 30 | 31 | pgv_cc_proto_library( 32 | name = "cc", 33 | visibility = ["//tests:__subpackages__"], 34 | deps = [":embed_proto"], 35 | ) 36 | 37 | proto_library( 38 | name = "yet_another_package_proto", 39 | srcs = ["embed.proto"], 40 | visibility = ["//visibility:public"], 41 | deps = ["//validate:validate_proto"], 42 | ) 43 | 44 | go_proto_library( 45 | name = "yet_another_package_go_proto", 46 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/yet_another_package", 47 | proto = ":yet_another_package_proto", 48 | visibility = ["//visibility:public"], 49 | deps = ["//validate:go_default_library"], 50 | ) 51 | 52 | go_library( 53 | name = "go_default_library", 54 | embed = [":yet_another_package_go_proto"], 55 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/yet_another_package", 56 | visibility = ["//visibility:public"], 57 | ) 58 | 59 | java_proto_library( 60 | name = "embed_java_proto", 61 | visibility = ["//visibility:public"], 62 | deps = [":embed_proto"], 63 | ) 64 | 65 | pgv_java_proto_library( 66 | name = "java", 67 | java_deps = [":embed_java_proto"], 68 | visibility = ["//visibility:public"], 69 | deps = [":embed_proto"], 70 | ) 71 | 72 | py_proto_library( 73 | name = "embed_python_proto", 74 | srcs = ["embed.proto"], 75 | visibility = ["//visibility:public"], 76 | deps = [ 77 | "//validate:validate_py", 78 | "@com_google_protobuf//:protobuf_python", 79 | ], 80 | ) 81 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cases/yet_another_package/embed.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness.cases.yet_another_package; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/yet_another_package/go;yet_another_package"; 5 | 6 | import "validate/validate.proto"; 7 | 8 | // Validate message embedding across packages. 9 | message Embed { 10 | int64 val = 1 [(validate.rules).int64.gt = 0]; 11 | 12 | enum Enumerated { VALUE = 0; } 13 | } 14 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cc/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") 2 | 3 | MSVC_C_OPTS = [ 4 | "-WX", 5 | "-DWIN32", 6 | "-DWIN32_LEAN_AND_MEAN", 7 | ] 8 | 9 | POSIX_C_OPTS = [ 10 | "-Wall", 11 | "-Wextra", 12 | "-Werror", 13 | "-Wnon-virtual-dtor", 14 | "-Woverloaded-virtual", 15 | "-Wold-style-cast", 16 | "-std=c++14", 17 | ] 18 | 19 | config_setting( 20 | name = "windows_x86_64", 21 | values = {"cpu": "x64_windows"}, 22 | ) 23 | 24 | cc_binary( 25 | name = "cc-harness", 26 | srcs = ["harness.cc"], 27 | # These ensure that we are at least compatible with what Envoy is expecting. 28 | copts = select({ 29 | ":windows_x86_64": MSVC_C_OPTS, 30 | "//conditions:default": POSIX_C_OPTS, 31 | }), 32 | visibility = ["//visibility:public"], 33 | deps = [ 34 | "//tests/harness:harness_cc_proto", 35 | "//tests/harness/cases:cc", 36 | ], 37 | ) 38 | 39 | # Ensure that if the headers are included in multiple libraries, those libraries 40 | # can be linked without conflicts. 41 | cc_test( 42 | name = "cc_diamond_test", 43 | srcs = ["diamond_test.cc"], 44 | copts = select({ 45 | ":windows_x86_64": MSVC_C_OPTS, 46 | "//conditions:default": POSIX_C_OPTS, 47 | }), 48 | linkstatic = 1, # Forces both libraries to be linked in. DO NOT REMOVE THIS 49 | deps = [ 50 | "cc_diamond_0", 51 | "cc_diamond_1", 52 | ], 53 | ) 54 | 55 | cc_library( 56 | name = "cc_diamond_0", 57 | srcs = ["diamond_lib.cc"], 58 | copts = select({ 59 | ":windows_x86_64": MSVC_C_OPTS, 60 | "//conditions:default": POSIX_C_OPTS, 61 | }), 62 | deps = ["//tests/harness/cases:cc"], 63 | alwayslink = 1, 64 | ) 65 | 66 | cc_library( 67 | name = "cc_diamond_1", 68 | srcs = ["diamond_lib.cc"], 69 | copts = select({ 70 | ":windows_x86_64": MSVC_C_OPTS, 71 | "//conditions:default": POSIX_C_OPTS, 72 | }), 73 | deps = ["//tests/harness/cases:cc"], 74 | alwayslink = 1, 75 | ) 76 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cc/diamond_lib.cc: -------------------------------------------------------------------------------- 1 | #include "tests/harness/cases/bool.pb.h" 2 | #include "tests/harness/cases/bool.pb.validate.h" 3 | #include "tests/harness/cases/bytes.pb.h" 4 | #include "tests/harness/cases/bytes.pb.validate.h" 5 | #include "tests/harness/cases/enums.pb.h" 6 | #include "tests/harness/cases/enums.pb.validate.h" 7 | #include "tests/harness/cases/maps.pb.h" 8 | #include "tests/harness/cases/messages.pb.h" 9 | #include "tests/harness/cases/messages.pb.validate.h" 10 | #include "tests/harness/cases/numbers.pb.h" 11 | #include "tests/harness/cases/numbers.pb.validate.h" 12 | #include "tests/harness/cases/oneofs.pb.h" 13 | #include "tests/harness/cases/oneofs.pb.validate.h" 14 | #include "tests/harness/cases/repeated.pb.h" 15 | #include "tests/harness/cases/repeated.pb.validate.h" 16 | #include "tests/harness/cases/strings.pb.h" 17 | #include "tests/harness/cases/strings.pb.validate.h" 18 | #include "tests/harness/cases/wkt_any.pb.h" 19 | #include "tests/harness/cases/wkt_any.pb.validate.h" 20 | #include "tests/harness/cases/wkt_duration.pb.h" 21 | #include "tests/harness/cases/wkt_duration.pb.validate.h" 22 | #include "tests/harness/cases/wkt_nested.pb.h" 23 | #include "tests/harness/cases/wkt_nested.pb.validate.h" 24 | #include "tests/harness/cases/wkt_timestamp.pb.h" 25 | #include "tests/harness/cases/wkt_wrappers.pb.h" 26 | #include "tests/harness/cases/wkt_wrappers.pb.validate.h" 27 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cc/diamond_test.cc: -------------------------------------------------------------------------------- 1 | #include "tests/harness/cases/bool.pb.h" 2 | #include "tests/harness/cases/bool.pb.validate.h" 3 | #include "tests/harness/cases/bytes.pb.h" 4 | #include "tests/harness/cases/bytes.pb.validate.h" 5 | #include "tests/harness/cases/enums.pb.h" 6 | #include "tests/harness/cases/enums.pb.validate.h" 7 | #include "tests/harness/cases/maps.pb.h" 8 | #include "tests/harness/cases/messages.pb.h" 9 | #include "tests/harness/cases/messages.pb.validate.h" 10 | #include "tests/harness/cases/numbers.pb.h" 11 | #include "tests/harness/cases/numbers.pb.validate.h" 12 | #include "tests/harness/cases/oneofs.pb.h" 13 | #include "tests/harness/cases/oneofs.pb.validate.h" 14 | #include "tests/harness/cases/repeated.pb.h" 15 | #include "tests/harness/cases/repeated.pb.validate.h" 16 | #include "tests/harness/cases/strings.pb.h" 17 | #include "tests/harness/cases/strings.pb.validate.h" 18 | #include "tests/harness/cases/wkt_any.pb.h" 19 | #include "tests/harness/cases/wkt_any.pb.validate.h" 20 | #include "tests/harness/cases/wkt_duration.pb.h" 21 | #include "tests/harness/cases/wkt_duration.pb.validate.h" 22 | #include "tests/harness/cases/wkt_nested.pb.h" 23 | #include "tests/harness/cases/wkt_nested.pb.validate.h" 24 | #include "tests/harness/cases/wkt_timestamp.pb.h" 25 | #include "tests/harness/cases/wkt_wrappers.pb.h" 26 | #include "tests/harness/cases/wkt_wrappers.pb.validate.h" 27 | 28 | int main(int argc, char **argv) { 29 | (void)argc; 30 | (void)argv; 31 | 32 | return EXIT_SUCCESS; 33 | } 34 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/cc/harness.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #if defined(WIN32) 5 | #include 6 | #include 7 | #include 8 | #endif 9 | 10 | #include "validate/validate.h" 11 | 12 | #include "tests/harness/cases/bool.pb.h" 13 | #include "tests/harness/cases/bool.pb.validate.h" 14 | #include "tests/harness/cases/bytes.pb.h" 15 | #include "tests/harness/cases/bytes.pb.validate.h" 16 | #include "tests/harness/cases/enums.pb.h" 17 | #include "tests/harness/cases/enums.pb.validate.h" 18 | #include "tests/harness/cases/filename-with-dash.pb.h" 19 | #include "tests/harness/cases/filename-with-dash.pb.validate.h" 20 | #include "tests/harness/cases/maps.pb.h" 21 | #include "tests/harness/cases/maps.pb.validate.h" 22 | #include "tests/harness/cases/messages.pb.h" 23 | #include "tests/harness/cases/messages.pb.validate.h" 24 | #include "tests/harness/cases/numbers.pb.h" 25 | #include "tests/harness/cases/numbers.pb.validate.h" 26 | #include "tests/harness/cases/oneofs.pb.h" 27 | #include "tests/harness/cases/oneofs.pb.validate.h" 28 | #include "tests/harness/cases/repeated.pb.h" 29 | #include "tests/harness/cases/repeated.pb.validate.h" 30 | #include "tests/harness/cases/strings.pb.h" 31 | #include "tests/harness/cases/strings.pb.validate.h" 32 | #include "tests/harness/cases/wkt_any.pb.h" 33 | #include "tests/harness/cases/wkt_any.pb.validate.h" 34 | #include "tests/harness/cases/wkt_duration.pb.h" 35 | #include "tests/harness/cases/wkt_duration.pb.validate.h" 36 | #include "tests/harness/cases/wkt_nested.pb.h" 37 | #include "tests/harness/cases/wkt_nested.pb.validate.h" 38 | #include "tests/harness/cases/wkt_timestamp.pb.h" 39 | #include "tests/harness/cases/wkt_timestamp.pb.validate.h" 40 | #include "tests/harness/cases/wkt_wrappers.pb.h" 41 | #include "tests/harness/cases/wkt_wrappers.pb.validate.h" 42 | #include "tests/harness/cases/kitchen_sink.pb.h" 43 | #include "tests/harness/cases/kitchen_sink.pb.validate.h" 44 | 45 | #include "tests/harness/harness.pb.h" 46 | 47 | // These macros are defined in the various validation headers and call the 48 | // X macro once for each message class in the header. Add macros here with new 49 | // pb.validate.h headers. 50 | #define X_TESTS_HARNESS_CASES(X) \ 51 | X_TESTS_HARNESS_CASES_BOOL(X) \ 52 | X_TESTS_HARNESS_CASES_BYTES(X) \ 53 | X_TESTS_HARNESS_CASES_ENUMS(X) \ 54 | X_TESTS_HARNESS_CASES_FILENAME_WITH_DASH(X) \ 55 | X_TESTS_HARNESS_CASES_MAPS(X) \ 56 | X_TESTS_HARNESS_CASES_MESSAGES(X) \ 57 | X_TESTS_HARNESS_CASES_NUMBERS(X) \ 58 | X_TESTS_HARNESS_CASES_ONEOFS(X) \ 59 | X_TESTS_HARNESS_CASES_REPEATED(X) \ 60 | X_TESTS_HARNESS_CASES_STRINGS(X) \ 61 | X_TESTS_HARNESS_CASES_WKT_ANY(X) \ 62 | X_TESTS_HARNESS_CASES_WKT_DURATION(X) \ 63 | X_TESTS_HARNESS_CASES_WKT_NESTED(X) \ 64 | X_TESTS_HARNESS_CASES_WKT_TIMESTAMP(X) \ 65 | X_TESTS_HARNESS_CASES_WKT_WRAPPERS(X) \ 66 | X_TESTS_HARNESS_CASES_KITCHEN_SINK(X) \ 67 | 68 | namespace { 69 | 70 | using tests::harness::TestCase; 71 | using tests::harness::TestResult; 72 | using google::protobuf::Any; 73 | 74 | std::ostream& operator<<(std::ostream& out, const TestResult& result) { 75 | if (result.reasons_size() > 0) { 76 | out << "valid: " << result.valid() << " reason: '" << result.reasons(0) << "'" 77 | << std::endl; 78 | } else { 79 | out << "valid: " << result.valid() << " reason: unknown" 80 | << std::endl; 81 | } 82 | return out; 83 | } 84 | 85 | void WriteTestResultAndExit(const TestResult& result) { 86 | if (!result.SerializeToOstream(&std::cout)) { 87 | std::cerr << "could not martial response: "; 88 | std::cerr << result << std::endl; 89 | exit(EXIT_FAILURE); 90 | } 91 | exit(EXIT_SUCCESS); 92 | } 93 | 94 | void ExitIfFailed(bool succeeded, const pgv::ValidationMsg& err_msg) { 95 | if (succeeded) { 96 | return; 97 | } 98 | 99 | TestResult result; 100 | result.set_error(true); 101 | result.add_reasons(pgv::String(err_msg)); 102 | WriteTestResultAndExit(result); 103 | } 104 | 105 | std::function GetValidationCheck(const Any& msg) { 106 | // This macro is intended to be called once for each message type with the 107 | // fully-qualified class name passed in as the only argument CLS. It checks 108 | // whether the msg argument above can be unpacked as a CLS. If so, it returns 109 | // a lambda that, when called, unpacks the message and validates it as a CLS. 110 | // This is here to work around the lack of duck-typing in C++, and because the 111 | // validation function can't be specified as a virtual method on the 112 | // google::protobuf::Message class. 113 | #define TRY_RETURN_VALIDATE_CALLABLE(CLS) \ 114 | if (msg.Is()) { \ 115 | return [msg] () { \ 116 | pgv::ValidationMsg err_msg; \ 117 | TestResult result; \ 118 | CLS unpacked; \ 119 | msg.UnpackTo(&unpacked); \ 120 | try { \ 121 | result.set_valid(Validate(unpacked, &err_msg)); \ 122 | result.add_reasons(std::move(err_msg)); \ 123 | } catch (pgv::UnimplementedException& e) { \ 124 | /* don't fail for unimplemented validations */ \ 125 | result.set_valid(false); \ 126 | result.set_allowfailure(true); \ 127 | result.add_reasons(e.what()); \ 128 | } \ 129 | return result; \ 130 | }; \ 131 | } 132 | 133 | X_TESTS_HARNESS_CASES(TRY_RETURN_VALIDATE_CALLABLE) 134 | #undef TRY_RETURN_VALIDATE_CALLABLE 135 | 136 | // Special handling for ignored messages, which don't have any code generated 137 | // for them. 138 | if (msg.Is<::tests::harness::cases::MessageIgnored>()) { 139 | return []() { 140 | TestResult result; 141 | result.set_valid(true); 142 | result.set_allowfailure(true); 143 | result.add_reasons("no validation possible for ignored messages"); 144 | return result; 145 | }; 146 | } 147 | 148 | // By default, return a null callable to signal that the message cannot be 149 | // handled. 150 | return nullptr; 151 | } 152 | 153 | } // namespace 154 | 155 | int main() { 156 | TestCase test_case; 157 | 158 | #if defined(WIN32) 159 | // need to explicitly set the stdin file mode to binary on Windows 160 | ExitIfFailed(_setmode(_fileno(stdin), _O_BINARY) != -1, "failed to set stdin to binary mode"); 161 | #endif 162 | 163 | ExitIfFailed(test_case.ParseFromIstream(&std::cin), "failed to parse TestCase"); 164 | 165 | auto validate_fn = GetValidationCheck(test_case.message()); 166 | if (validate_fn == nullptr) { 167 | std::cerr << "No known validator for message type " 168 | << test_case.message().type_url() 169 | << "; did you add it to the harness?"; 170 | return 1; 171 | } 172 | 173 | WriteTestResultAndExit(validate_fn()); 174 | 175 | return 0; 176 | } 177 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/executor/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | config_setting( 4 | name = "windows_x86_64", 5 | values = {"cpu": "x64_windows"}, 6 | ) 7 | 8 | go_library( 9 | name = "go_default_library", 10 | srcs = [ 11 | "cases.go", 12 | "executor.go", 13 | "harness.go", 14 | "worker.go", 15 | ], 16 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/executor", 17 | visibility = ["//visibility:private"], 18 | deps = [ 19 | "//tests/harness:harness_go_proto", 20 | "//tests/harness/cases:go", 21 | "//tests/harness/cases/other_package:go", 22 | "//tests/harness/cases/yet_another_package:go", 23 | "@org_golang_google_protobuf//proto:go_default_library", 24 | "@org_golang_google_protobuf//types/known/anypb:go_default_library", 25 | "@org_golang_google_protobuf//types/known/durationpb:go_default_library", 26 | "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", 27 | "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library", 28 | "@org_golang_x_net//context:go_default_library", 29 | ], 30 | ) 31 | 32 | go_binary( 33 | name = "executor", 34 | data = ["//tests/harness/cc:cc-harness"] + select({ 35 | ":windows_x86_64": [ 36 | "//tests/harness/go/main:go-harness-exe", 37 | ], 38 | "//conditions:default": [ 39 | "//tests/harness/go/main:go-harness-bin", 40 | "//tests/harness/java:java-harness", 41 | "//tests/harness/python:python-harness", 42 | ], 43 | }), 44 | embed = [":go_default_library"], 45 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/executor", 46 | visibility = ["//visibility:private"], 47 | ) 48 | 49 | [ 50 | sh_test( 51 | name = "executor_" + lang + "_test", 52 | srcs = ["executor_test.sh"], 53 | args = [ 54 | "$(location :executor)", 55 | "-" + lang, 56 | ], 57 | data = [":executor"], 58 | # This could be sharded more, but each shard incurs overhead and test 59 | # execution is already sharded by having separate test rules for each language. 60 | shard_count = 5, 61 | deps = ["@bazel_tools//tools/bash/runfiles"], 62 | ) 63 | for lang in ("cc", "go", "java", "python") 64 | ] 65 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/executor/executor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "math" 7 | "os" 8 | "runtime" 9 | "strconv" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | ) 14 | 15 | func init() { 16 | log.SetFlags(0) 17 | } 18 | 19 | func main() { 20 | parallelism := flag.Int("parallelism", runtime.NumCPU(), "Number of test cases to run in parallel") 21 | goFlag := flag.Bool("go", false, "Run go test harness") 22 | ccFlag := flag.Bool("cc", false, "Run c++ test harness") 23 | javaFlag := flag.Bool("java", false, "Run java test harness") 24 | pythonFlag := flag.Bool("python", false, "Run python test harness") 25 | externalHarnessFlag := flag.String("external_harness", "", "Path to a binary to be executed as an external test harness") 26 | flag.Parse() 27 | 28 | test_cases := shardTestCases(TestCases) 29 | start := time.Now() 30 | harnesses := Harnesses(*goFlag, *ccFlag, *javaFlag, *pythonFlag, *externalHarnessFlag) 31 | successes, failures, skips := run(*parallelism, harnesses, test_cases) 32 | 33 | log.Printf("Successes: %d | Failures: %d | Skips: %d (%v)", 34 | successes, failures, skips, time.Since(start)) 35 | 36 | if failures > 0 { 37 | os.Exit(1) 38 | } 39 | } 40 | 41 | func shardTestCases(test_cases []TestCase) []TestCase { 42 | // Support Bazel test sharding by slicing up the list of test cases if requested. 43 | shard_count, err := strconv.Atoi(os.Getenv("TEST_TOTAL_SHARDS")) 44 | if err != nil { 45 | return test_cases 46 | } 47 | 48 | shard_index, err := strconv.Atoi(os.Getenv("TEST_SHARD_INDEX")) 49 | if err != nil { 50 | return test_cases 51 | } 52 | 53 | // Bazel expects that the test will create or modify the file with the provided name to show that it supports sharding. 54 | status_file := os.Getenv("TEST_SHARD_STATUS_FILE") 55 | if status_file == "" { 56 | return test_cases 57 | } 58 | 59 | if file, err := os.Create(status_file); err != nil { 60 | return test_cases 61 | } else { 62 | file.Close() 63 | } 64 | shard_length := int(math.Ceil(float64(len(test_cases)) / float64(shard_count))) 65 | shard_start := shard_index * shard_length 66 | shard_end := (shard_index + 1) * shard_length 67 | if shard_end >= len(test_cases) { 68 | shard_end = len(test_cases) 69 | } 70 | return test_cases[shard_start:shard_end] 71 | } 72 | 73 | func run(parallelism int, harnesses []Harness, test_cases []TestCase) (successes, failures, skips uint64) { 74 | wg := new(sync.WaitGroup) 75 | if parallelism <= 0 { 76 | panic("Parallelism must be > 0") 77 | } 78 | if len(harnesses) == 0 { 79 | panic("At least one harness must be selected with a flag") 80 | } 81 | wg.Add(parallelism) 82 | 83 | in := make(chan TestCase) 84 | out := make(chan TestResult) 85 | done := make(chan struct{}) 86 | 87 | for i := 0; i < parallelism; i++ { 88 | go Work(wg, in, out, harnesses) 89 | } 90 | 91 | go func() { 92 | for res := range out { 93 | if res.Skipped { 94 | atomic.AddUint64(&skips, 1) 95 | } else if res.OK { 96 | atomic.AddUint64(&successes, 1) 97 | } else { 98 | atomic.AddUint64(&failures, 1) 99 | } 100 | } 101 | close(done) 102 | }() 103 | 104 | for _, test := range test_cases { 105 | in <- test 106 | } 107 | close(in) 108 | 109 | wg.Wait() 110 | close(out) 111 | <-done 112 | 113 | return 114 | } 115 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/executor/executor_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | set -x 4 | 5 | EXECUTOR_BIN=$1 6 | shift 7 | 8 | $EXECUTOR_BIN "$@" 9 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/executor/harness.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | 14 | harness "github.com/envoyproxy/protoc-gen-validate/tests/harness/go" 15 | "golang.org/x/net/context" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | func Harnesses(goFlag, ccFlag, javaFlag, pythonFlag bool, externalHarnessFlag string) []Harness { 20 | harnesses := make([]Harness, 0) 21 | if goFlag { 22 | harnesses = append(harnesses, InitHarness("tests/harness/go/main/go-harness", "go")) 23 | } 24 | if ccFlag { 25 | harnesses = append(harnesses, InitHarness("tests/harness/cc/cc-harness", "cc")) 26 | } 27 | if javaFlag { 28 | harnesses = append(harnesses, InitHarness("tests/harness/java/java-harness", "java")) 29 | } 30 | if pythonFlag { 31 | harnesses = append(harnesses, InitHarness("tests/harness/python/python-harness", "python")) 32 | } 33 | if externalHarnessFlag != "" { 34 | harnesses = append(harnesses, InitHarness(externalHarnessFlag, "external")) 35 | } 36 | return harnesses 37 | } 38 | 39 | type Harness struct { 40 | Name string 41 | Exec func(context.Context, io.Reader) (*harness.TestResult, error) 42 | } 43 | 44 | func InitHarness(cmd string, name string, args ...string) Harness { 45 | if runtime.GOOS == "windows" { 46 | // Bazel runfiles are not symlinked in on windows, 47 | // so we have to use the manifest instead. If the manifest 48 | // doesn't exist, assume we're running in a non-Bazel context 49 | f, err := os.Open("MANIFEST") 50 | if err == nil { 51 | defer f.Close() 52 | 53 | s := bufio.NewScanner(f) 54 | manifest := map[string]string{} 55 | for s.Scan() { 56 | values := strings.Split(s.Text(), " ") 57 | manifest[values[0]] = values[1] 58 | } 59 | for k, v := range manifest { 60 | if strings.Contains(k, cmd) { 61 | cmd = v 62 | } 63 | } 64 | } 65 | } 66 | 67 | return Harness{ 68 | Name: name, 69 | Exec: initHarness(cmd, args...), 70 | } 71 | } 72 | 73 | func initHarness(cmd string, args ...string) func(context.Context, io.Reader) (*harness.TestResult, error) { 74 | return func(ctx context.Context, r io.Reader) (*harness.TestResult, error) { 75 | out, errs := getBuf(), getBuf() 76 | defer relBuf(out) 77 | defer relBuf(errs) 78 | 79 | cmd := exec.CommandContext(ctx, cmd, args...) 80 | cmd.Stdin = r 81 | cmd.Stdout = out 82 | cmd.Stderr = errs 83 | 84 | if err := cmd.Run(); err != nil { 85 | return nil, fmt.Errorf("[%s] failed execution (%v) - captured stderr:\n%s", cmdStr(cmd), err, errs.String()) 86 | } 87 | if errs.Len() > 0 { 88 | fmt.Printf("captured stderr:\n%s", errs.String()) 89 | } 90 | 91 | res := new(harness.TestResult) 92 | if err := proto.Unmarshal(out.Bytes(), res); err != nil { 93 | return nil, fmt.Errorf("[%s] failed to unmarshal result: %v", cmdStr(cmd), err) 94 | } 95 | 96 | return res, nil 97 | } 98 | } 99 | 100 | var bufPool = &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} 101 | 102 | func getBuf() *bytes.Buffer { 103 | return bufPool.Get().(*bytes.Buffer) 104 | } 105 | 106 | func relBuf(b *bytes.Buffer) { 107 | b.Reset() 108 | bufPool.Put(b) 109 | } 110 | 111 | func cmdStr(cmd *exec.Cmd) string { 112 | return fmt.Sprintf("%s %s", cmd.Path, strings.Join(cmd.Args, " ")) 113 | } 114 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/executor/worker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "log" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | harness "github.com/envoyproxy/protoc-gen-validate/tests/harness/go" 12 | "google.golang.org/protobuf/proto" 13 | "google.golang.org/protobuf/types/known/anypb" 14 | ) 15 | 16 | func Work(wg *sync.WaitGroup, in <-chan TestCase, out chan<- TestResult, harnesses []Harness) { 17 | for tc := range in { 18 | execTestCase(tc, harnesses, out) 19 | } 20 | wg.Done() 21 | } 22 | 23 | func execTestCase(tc TestCase, harnesses []Harness, out chan<- TestResult) { 24 | any, err := anypb.New(tc.Message) 25 | if err != nil { 26 | log.Printf("unable to convert test case %q to Any - %v", tc.Name, err) 27 | out <- TestResult{false, false} 28 | return 29 | } 30 | 31 | b, err := proto.Marshal(&harness.TestCase{Message: any}) 32 | if err != nil { 33 | log.Printf("unable to marshal test case %q - %v", tc.Name, err) 34 | out <- TestResult{false, false} 35 | return 36 | } 37 | 38 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 39 | defer cancel() 40 | 41 | wg := new(sync.WaitGroup) 42 | wg.Add(len(harnesses)) 43 | 44 | for _, h := range harnesses { 45 | h := h 46 | go func() { 47 | defer wg.Done() 48 | 49 | res, err := h.Exec(ctx, bytes.NewReader(b)) 50 | if err != nil { 51 | log.Printf("[%s] (%s harness) executor error: %s", tc.Name, h.Name, err.Error()) 52 | out <- TestResult{false, false} 53 | return 54 | } 55 | 56 | if res.Error { 57 | log.Printf("[%s] (%s harness) internal harness error: %s", tc.Name, h.Name, res.Reasons) 58 | out <- TestResult{false, false} 59 | return 60 | } 61 | 62 | // Backwards compatibility for languages with no multi-error 63 | // feature: check results of validation in "fail fast" mode only 64 | if !res.CheckMultipleErrors { 65 | tcValid := tc.Failures == 0 66 | if res.Valid != tcValid { 67 | if res.AllowFailure { 68 | log.Printf("[%s] (%s harness) ignoring test failure: %v", tc.Name, h.Name, res.Reasons) 69 | out <- TestResult{false, true} 70 | } else if tcValid { 71 | log.Printf("[%s] (%s harness) expected valid, got invalid: %v", tc.Name, h.Name, res.Reasons) 72 | out <- TestResult{false, false} 73 | } else { 74 | log.Printf("[%s] (%s harness) expected invalid, got valid: %v", tc.Name, h.Name, res.Reasons) 75 | out <- TestResult{false, false} 76 | } 77 | } else { 78 | out <- TestResult{true, false} 79 | } 80 | return 81 | } 82 | 83 | // Check results of validation in "extensive" mode 84 | if len(res.Reasons) != tc.Failures { 85 | if res.AllowFailure { 86 | log.Printf("[%s] (%s harness) ignoring bad number of failures: %v", tc.Name, h.Name, res.Reasons) 87 | out <- TestResult{false, true} 88 | } else { 89 | log.Printf("[%s] (%s harness) expected %d failures, got %d:\n %v", tc.Name, h.Name, tc.Failures, len(res.Reasons), strings.Join(res.Reasons, "\n ")) 90 | out <- TestResult{false, false} 91 | } 92 | return 93 | } 94 | 95 | out <- TestResult{true, false} 96 | }() 97 | } 98 | 99 | wg.Wait() 100 | return 101 | } 102 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/go/main/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["harness.go"], 6 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/go/main", 7 | visibility = ["//visibility:private"], 8 | deps = [ 9 | "//tests/harness:harness_go_proto", 10 | "//tests/harness/cases:go", 11 | "//tests/harness/cases/other_package:go", 12 | "//tests/harness/cases/yet_another_package:go", 13 | "@org_golang_google_protobuf//proto:go_default_library", 14 | "@org_golang_google_protobuf//types/known/anypb:go_default_library", 15 | "@org_golang_google_protobuf//types/known/durationpb:go_default_library", 16 | "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", 17 | ], 18 | ) 19 | 20 | genrule( 21 | name = "go-harness-bin", 22 | srcs = [":main"], 23 | outs = ["go-harness"], 24 | cmd = "cp $(SRCS) $@", 25 | visibility = ["//visibility:public"], 26 | ) 27 | 28 | genrule( 29 | name = "go-harness-exe", 30 | srcs = [":main"], 31 | outs = ["go-harness.exe"], 32 | cmd = "cp $(SRCS) $@", 33 | visibility = ["//visibility:public"], 34 | ) 35 | 36 | go_binary( 37 | name = "main", 38 | embed = [":go_default_library"], 39 | importpath = "github.com/envoyproxy/protoc-gen-validate/tests/harness/go/main", 40 | visibility = ["//visibility:public"], 41 | ) 42 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/go/main/harness.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go" 10 | _ "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/go" 11 | _ "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/other_package/go" 12 | _ "github.com/envoyproxy/protoc-gen-validate/tests/harness/cases/yet_another_package/go" 13 | "github.com/envoyproxy/protoc-gen-validate/tests/harness/go" 14 | "google.golang.org/protobuf/proto" 15 | ) 16 | 17 | func main() { 18 | b, err := ioutil.ReadAll(os.Stdin) 19 | checkErr(err) 20 | 21 | tc := new(harness.TestCase) 22 | checkErr(proto.Unmarshal(b, tc)) 23 | 24 | msg, err := tc.Message.UnmarshalNew() 25 | checkErr(err) 26 | 27 | _, isIgnored := msg.(*cases.MessageIgnored) 28 | 29 | vMsg, hasValidate := msg.(interface { 30 | Validate() error 31 | }) 32 | 33 | vAllMsg, hasValidateAll := msg.(interface { 34 | ValidateAll() error 35 | }) 36 | 37 | var multierr error 38 | if isIgnored { 39 | // confirm that ignored messages don't have a validate method 40 | if hasValidate { 41 | checkErr(fmt.Errorf("ignored message %T has Validate() method", msg)) 42 | } 43 | if hasValidateAll { 44 | checkErr(fmt.Errorf("ignored message %T has ValidateAll() method", msg)) 45 | } 46 | } else if !hasValidate { 47 | checkErr(fmt.Errorf("non-ignored message %T is missing Validate()", msg)) 48 | } else if !hasValidateAll { 49 | checkErr(fmt.Errorf("non-ignored message %T is missing ValidateAll()", msg)) 50 | } else { 51 | err = vMsg.Validate() 52 | multierr = vAllMsg.ValidateAll() 53 | } 54 | checkValid(err, multierr) 55 | } 56 | 57 | type hasAllErrors interface{ AllErrors() []error } 58 | type hasCause interface{ Cause() error } 59 | 60 | func checkValid(err, multierr error) { 61 | if err == nil && multierr == nil { 62 | resp(&harness.TestResult{Valid: true}) 63 | return 64 | } 65 | if (err != nil) != (multierr != nil) { 66 | checkErr(fmt.Errorf("different verdict of Validate() [%v] vs. ValidateAll() [%v]", err, multierr)) 67 | return 68 | } 69 | 70 | // Extract the message from "lazy" Validate(), for comparison with ValidateAll() 71 | rootCause := err 72 | for { 73 | caused, ok := rootCause.(hasCause) 74 | if !ok || caused.Cause() == nil { 75 | break 76 | } 77 | rootCause = caused.Cause() 78 | } 79 | 80 | // Retrieve the messages from "extensive" ValidateAll() and compare first one with the "lazy" message 81 | m, ok := multierr.(hasAllErrors) 82 | if !ok { 83 | checkErr(fmt.Errorf("ValidateAll() returned error without AllErrors() method: %#v", multierr)) 84 | return 85 | } 86 | reasons := mergeReasons(nil, m) 87 | if rootCause.Error() != reasons[0] { 88 | checkErr(fmt.Errorf("different first message, Validate()==%q, ValidateAll()==%q", rootCause.Error(), reasons[0])) 89 | return 90 | } 91 | 92 | resp(&harness.TestResult{Reasons: reasons}) 93 | } 94 | 95 | func mergeReasons(reasons []string, multi hasAllErrors) []string { 96 | for _, err := range multi.AllErrors() { 97 | caused, ok := err.(hasCause) 98 | if ok && caused.Cause() != nil { 99 | err = caused.Cause() 100 | } 101 | multi, ok := err.(hasAllErrors) 102 | if ok { 103 | reasons = mergeReasons(reasons, multi) 104 | } else { 105 | reasons = append(reasons, err.Error()) 106 | } 107 | } 108 | return reasons 109 | } 110 | 111 | func checkErr(err error) { 112 | if err == nil { 113 | return 114 | } 115 | 116 | resp(&harness.TestResult{ 117 | Error: true, 118 | Reasons: []string{err.Error()}, 119 | }) 120 | } 121 | 122 | func resp(result *harness.TestResult) { 123 | if b, err := proto.Marshal(result); err != nil { 124 | log.Fatalf("could not marshal response: %v", err) 125 | } else if _, err = os.Stdout.Write(b); err != nil { 126 | log.Fatalf("could not write response: %v", err) 127 | } 128 | 129 | os.Exit(0) 130 | } 131 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/harness.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package tests.harness; 4 | option go_package = "github.com/envoyproxy/protoc-gen-validate/tests/harness/go;harness"; 5 | 6 | import "google/protobuf/any.proto"; 7 | 8 | message TestCase { 9 | google.protobuf.Any message = 1; 10 | } 11 | 12 | message TestResult { 13 | bool Valid = 1; 14 | bool Error = 2; 15 | repeated string Reasons = 3; 16 | bool AllowFailure = 4; 17 | bool CheckMultipleErrors = 5; 18 | } 19 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/java/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_java//java:defs.bzl", "java_binary") 2 | 3 | java_binary( 4 | name = "java-harness", 5 | main_class = "io.envoyproxy.pgv.validation.JavaHarness", 6 | visibility = ["//tests/harness:__subpackages__"], 7 | runtime_deps = [ 8 | "//java/pgv-java-validation/src/main/java/io/envoyproxy/pgv/validation:java_harness", 9 | ], 10 | ) 11 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/python/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_python//python:defs.bzl", "py_binary", "py_test") 2 | load("@pgv_pip_deps//:requirements.bzl", "requirement") 3 | 4 | py_binary( 5 | name = "python-harness", 6 | srcs = ["harness.py"], 7 | main = "harness.py", 8 | srcs_version = "PY3", 9 | visibility = ["//visibility:public"], 10 | deps = [ 11 | # ensures we test with the package's own protobuf runtime specified in setup.cfg 12 | # and not the one riding on the py_proto_library dependencies 13 | requirement("protobuf"), 14 | "//tests/harness:harness_py_proto", 15 | "//tests/harness/cases:cases_py_proto", 16 | "//python:validator_py" 17 | ] 18 | ) 19 | 20 | py_test( 21 | name = "python-requirements-match", 22 | srcs = ["requirements_test.py"], 23 | main = "requirements_test.py", 24 | srcs_version = "PY3", 25 | data = ["//python:setup.cfg", "//python:requirements.in"], 26 | ) 27 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/python/harness.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | 4 | from python.protoc_gen_validate.validator import validate, ValidationFailed 5 | 6 | from tests.harness.harness_pb2 import TestCase, TestResult 7 | from tests.harness.cases.bool_pb2 import * 8 | from tests.harness.cases.bytes_pb2 import * 9 | from tests.harness.cases.enums_pb2 import * 10 | from tests.harness.cases.enums_pb2 import * 11 | from tests.harness.cases.messages_pb2 import * 12 | from tests.harness.cases.numbers_pb2 import * 13 | from tests.harness.cases.oneofs_pb2 import * 14 | from tests.harness.cases.repeated_pb2 import * 15 | from tests.harness.cases.strings_pb2 import * 16 | from tests.harness.cases.maps_pb2 import * 17 | from tests.harness.cases.wkt_any_pb2 import * 18 | from tests.harness.cases.wkt_duration_pb2 import * 19 | from tests.harness.cases.wkt_nested_pb2 import * 20 | from tests.harness.cases.wkt_wrappers_pb2 import * 21 | from tests.harness.cases.wkt_timestamp_pb2 import * 22 | from tests.harness.cases.kitchen_sink_pb2 import * 23 | 24 | 25 | message_classes = {} 26 | for k, v in inspect.getmembers(sys.modules[__name__], inspect.isclass): 27 | if 'DESCRIPTOR' in dir(v): 28 | message_classes[v.DESCRIPTOR.full_name] = v 29 | 30 | 31 | if __name__ == "__main__": 32 | read = sys.stdin.buffer.read() 33 | 34 | testcase = TestCase() 35 | testcase.ParseFromString(read) 36 | 37 | test_class = message_classes[testcase.message.TypeName()] 38 | test_msg = test_class() 39 | testcase.message.Unpack(test_msg) 40 | 41 | try: 42 | result = TestResult() 43 | valid = validate(test_msg) 44 | result.Valid = True 45 | except ValidationFailed as e: 46 | result.Valid = False 47 | result.Reasons[:] = [repr(e)] 48 | 49 | sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf8') 50 | sys.stdout.write(result.SerializeToString().decode("utf-8")) 51 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/tests/harness/python/requirements_test.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | 3 | # There's two sets of requirements relevant for python. The first set in requirements.txt is installed 4 | # during the Docker build and is used for linting, building, and uploading the PGV python package to PyPI. 5 | # 6 | # The other set is in the install_requires section of setup.cfg. This is what's needed to use the package. 7 | # 8 | # We use pip_install from @rules_python to install these requirements in order to test the package. Unfortunately: 9 | # - pip_install can't handle setup.cfg directly, it wants a file containing a simple list 10 | # - as a bazel repository_rule, pip_install won't accept generated files as input so we can't autogen 11 | # this simpler file out of setup.cfg as part of bazel build. 12 | # 13 | # So instead here we just check that requirements.in matches what's in install_requires of setup.cfg. 14 | 15 | 16 | with open('python/requirements.in', 'r') as reqs: 17 | lines = reqs.readlines() 18 | requirements_dot_in_set = {line.strip() for line in lines if line.strip() and not line.startswith("#")} 19 | 20 | config = configparser.ConfigParser() 21 | config.read('python/setup.cfg') 22 | setup_dot_cfg_set = {line for line in config['options']['install_requires'].split() if not line.startswith("#")} 23 | 24 | assert requirements_dot_in_set == setup_dot_cfg_set 25 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/transforms/field.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package scalapb.transforms; 4 | 5 | import "validate/validate.proto"; 6 | import "scalapb/scalapb.proto"; 7 | 8 | message MyTestMessage { 9 | int32 opt_pos_num = 1 [(validate.rules) = {int32: {gt: 0}}]; 10 | repeated int32 rep_pos_num = 2 [(validate.rules).repeated.items.int32 = {gt: 0}]; 11 | repeated int32 set_pos_num = 3 [ 12 | (validate.rules).repeated = 13 | { 14 | items: {int32: {gt: 0}}, 15 | unique: true 16 | } 17 | ]; 18 | map map_pos_nums = 4 [ 19 | (validate.rules).map.keys.int32 = {gt: 0}, 20 | (validate.rules).map.values.int32 = {gt: 0} 21 | ]; 22 | int32 foo = 5 [(validate.rules).int32 = {gt: -5}]; 23 | } 24 | 25 | message MyMsg { 26 | string a = 1 [(validate.rules) = {string: {const: "boo"}}];; 27 | } 28 | 29 | message MyTestMessageWithNonEmpty { 30 | int32 opt_pos_num = 1 [(validate.rules) = {int32: {gt: 0}}]; 31 | repeated int32 rep_pos_num = 2 [ 32 | (validate.rules).repeated.items.int32 = {gt: 0}, 33 | (validate.rules).repeated.min_items = 1 34 | ]; 35 | repeated int32 set_pos_num = 3 [ 36 | (validate.rules).repeated.items.int32 = {gt: 0}, 37 | (validate.rules).repeated.min_items = 1, 38 | (validate.rules).repeated.unique = true 39 | ]; 40 | map map_pos_nums = 4 [ 41 | (validate.rules).map.keys.int32 = {gt: 0}, 42 | (validate.rules).map.values.int32 = {gt: 0}, 43 | (validate.rules).map.min_pairs = 1 44 | ]; 45 | map map_msg = 5 [ 46 | (validate.rules).map.keys.int32 = {gt: 0}, 47 | (validate.rules).map.min_pairs = 1, 48 | (scalapb.field).value_type = "scalapb.transforms.MyCustomType" 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/transforms/options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package scalapb.transforms; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | import "scalapb/validate.proto"; 8 | 9 | option (scalapb.options) = { 10 | scope : PACKAGE 11 | preprocessors : [ "scalapb-validate-preprocessor" ] 12 | [scalapb.validate.file]{ 13 | validate_at_construction : true 14 | cats_transforms : true 15 | unique_to_set : true 16 | } 17 | field_transformations : [ { 18 | when { 19 | options { 20 | [validate.rules]{int32 : {gt : 0}} 21 | } 22 | } 23 | set : {[scalapb.field] {type : "scalapb.transforms.PositiveInt"}} 24 | } ] 25 | field_transformations : [ { 26 | when { 27 | options { 28 | [validate.rules]{message : {required : true}} 29 | } 30 | } 31 | set : {[scalapb.field] {required : true}} 32 | } ] 33 | }; 34 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/transforms/order.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package scalapb.transforms.order; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | preprocessors : [ "scalapb-validate-preprocessor" ], 10 | scope : PACKAGE 11 | 12 | // This test verifies that the last transformation that matches overrides the 13 | // previous ones 14 | field_transformations : [ { 15 | when : { 16 | options { 17 | [validate.rules]{int32 : {gt : 0}} 18 | } 19 | } 20 | set : {[scalapb.field] {type : "UndefinedTypeShouldNotCompile"}} 21 | } ] 22 | field_transformations : [ { 23 | when : { 24 | options { 25 | [validate.rules]{int32 : {gt : 0}} 26 | } 27 | } 28 | set : {[scalapb.field] { type : "UndefinedTypeShouldAlsoNotCompile"}} 29 | } ] 30 | field_transformations : [ { 31 | when : { 32 | options { 33 | [validate.rules]{int32 : {gt : 0}} 34 | } 35 | } 36 | set : {[scalapb.field] { type : "scalapb.transforms.PositiveInt"} } 37 | } ] 38 | }; 39 | 40 | message TestMessage { 41 | optional int32 a = 1 [ (.validate.rules).int32 = {gt : 0} ]; 42 | } 43 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/transforms/order2.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package scalapb.transforms.order.order2; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | option (scalapb.options) = { 9 | scope : PACKAGE 10 | field_transformations : [ { 11 | when : { 12 | options { 13 | [validate.rules]{int32 : {gt : 0}} 14 | } 15 | } 16 | set : { [scalapb.field] { type : "ShouldNotCompile" scala_name : "bam"}} 17 | } ] 18 | }; 19 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/transforms/order3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package scalapb.transforms.order.order2.order3; 4 | 5 | import "scalapb/scalapb.proto"; 6 | import "validate/validate.proto"; 7 | 8 | // This test verifies that the last transformation that matches overrides the 9 | // previous ones 10 | option (scalapb.options) = { 11 | field_transformations : { 12 | when : { 13 | options { 14 | [validate.rules]{int32 : {gt : 0}} 15 | } 16 | } 17 | set : {[scalapb.field] {type : "scalapb.transforms.PositiveInt"}} 18 | } 19 | }; 20 | 21 | message TestMessage { 22 | // Shold be named Bam (from order2), the local type "PositiveInt" should 23 | // override ShouldNotCompile from the upper levels. 24 | optional int32 x = 1 [ (.validate.rules).int32 = {gt : 0} ]; 25 | } 26 | -------------------------------------------------------------------------------- /e2e/src/main/protobuf/transforms/required.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package scalapb.transforms; 4 | 5 | import "validate/validate.proto"; 6 | import "scalapb/scalapb.proto"; 7 | 8 | option (scalapb.options) = {preprocessors: ["scalapb-validate-preprocessor"]}; 9 | 10 | message RequiredMsg {} 11 | 12 | message Container { 13 | RequiredMsg required_msg = 1 [(validate.rules).message.required = true]; 14 | } -------------------------------------------------------------------------------- /e2e/src/main/scala-2/scalapb/transforms/refined/package.scala: -------------------------------------------------------------------------------- 1 | package scalapb.transforms.refined 2 | 3 | import scalapb.TypeMapper 4 | import eu.timepit.refined.refineV 5 | import eu.timepit.refined.api.{Refined, Validate} 6 | import scalapb.validate.ValidationException 7 | 8 | package object refined { 9 | implicit def refinedType[T, V](implicit 10 | ev: Validate[T, V] 11 | ): TypeMapper[T, Refined[T, V]] = 12 | TypeMapper[T, Refined[T, V]](refineV(_) match { 13 | case Left(error) => throw new ValidationException(error) 14 | case Right(value) => value 15 | })(_.value) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /e2e/src/main/scala/e2e/cats/alltypes/package.scala: -------------------------------------------------------------------------------- 1 | package e2e.cats 2 | 3 | import e2e.cats.types.{Color, SubMsg} 4 | import com.google.protobuf.ByteString 5 | 6 | package object alltypes { 7 | implicit val subMsg: Ordering[SubMsg] = 8 | Ordering.fromLessThan[SubMsg]((x, y) => x.a < y.a) 9 | 10 | implicit val color: Ordering[Color] = 11 | Ordering.fromLessThan[Color]((x, y) => x.value < y.value) 12 | 13 | implicit val byteString: Ordering[ByteString] = 14 | Ordering.String.on[ByteString](_.toByteArray.toVector.mkString("-")) 15 | 16 | implicit val kernelOrderSubMsg: cats.kernel.Order[SubMsg] = 17 | cats.kernel.Order.fromOrdering(subMsg) 18 | 19 | implicit val kernelOrderColor: cats.kernel.Order[Color] = 20 | cats.kernel.Order.fromOrdering(color) 21 | 22 | implicit val kernelOrderByteString: cats.kernel.Order[ByteString] = 23 | cats.kernel.Order.fromOrdering(byteString) 24 | } 25 | -------------------------------------------------------------------------------- /e2e/src/main/scala/scalapb/package.scala: -------------------------------------------------------------------------------- 1 | package scalapb 2 | 3 | import test.oneofs.TestBigDecimal 4 | 5 | package object test { 6 | implicit val tm: TypeMapper[TestBigDecimal, BigDecimal] = 7 | TypeMapper[TestBigDecimal, BigDecimal](_ => BigDecimal(0.0))(_ => 8 | TestBigDecimal() 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /e2e/src/main/scala/scalapb/transforms/MyCustomType.scala: -------------------------------------------------------------------------------- 1 | package scalapb.transforms 2 | 3 | import scalapb.TypeMapper 4 | 5 | import scalapb.transforms.field.MyMsg 6 | 7 | final case class MyCustomType(a: String) 8 | 9 | object MyCustomType { 10 | implicit val tm: TypeMapper[MyMsg, MyCustomType] = 11 | TypeMapper[MyMsg, MyCustomType](pb => MyCustomType(pb.a))(custom => 12 | MyMsg(custom.a) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /e2e/src/main/scala/scalapb/transforms/PositiveInt.scala: -------------------------------------------------------------------------------- 1 | package scalapb.transforms 2 | 3 | import scalapb.TypeMapper 4 | 5 | final case class PositiveInt(n: Int) { 6 | assert(n > 0) 7 | } 8 | 9 | object PositiveInt { 10 | implicit val tm: TypeMapper[Int, PositiveInt] = 11 | TypeMapper[Int, PositiveInt](PositiveInt(_))(_.n) 12 | implicit val ordering: Ordering[PositiveInt] = 13 | Ordering.fromLessThan[PositiveInt]((x, y) => x.n < y.n) 14 | } 15 | -------------------------------------------------------------------------------- /e2e/src/test/scala-2/scalapb/validate/transforms/refined/RefinedSpec.scala/RefinedSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.transforms.refined 2 | 3 | import scalapb.transforms.refined.refined.RefinedTest 4 | import scalapb.validate.ValidationException 5 | import eu.timepit.refined.auto._ 6 | 7 | class RefinedSpec extends munit.FunSuite { 8 | val m = RefinedTest(gt5 = 6, constant = 17, oc = 37.0) 9 | 10 | test("Valid message is parsable") { 11 | assertEquals(RefinedTest.parseFrom(m.toByteArray), m) 12 | } 13 | 14 | test("Not instantiable with default values") { 15 | interceptMessage[ValidationException]("Predicate failed: (0 > 5).") { 16 | RefinedTest() 17 | } 18 | } 19 | 20 | test("Refined types correctly installed") { 21 | assertNoDiff( 22 | compileErrors("m.withGt5(3)"), 23 | """|error: Predicate failed: (3 > 5). 24 | |m.withGt5(3) 25 | | ^ 26 | |""".stripMargin 27 | ) 28 | 29 | assertNoDiff( 30 | compileErrors("m.withConstant(14)"), 31 | """|error: Predicate failed: (14 == 17). 32 | |m.withConstant(14) 33 | | ^ 34 | |""".stripMargin 35 | ) 36 | 37 | assertNoDiff( 38 | compileErrors("m.withOc(0.0)"), 39 | """|error: Left predicate of ((0.0 > 0.0) && !(0.0 > 100.0)) failed: Predicate failed: (0.0 > 0.0). 40 | |m.withOc(0.0) 41 | | ^ 42 | |""".stripMargin 43 | ) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/OptionsSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import examplepb.options.NoValidator 4 | 5 | class OptionsSpec extends munit.FunSuite { 6 | 7 | test("Person empty") { 8 | val error = 9 | compileErrors("Validator[NoValidator]") 10 | assert( 11 | error.contains( 12 | "could not find implicit value for evidence parameter of type scalapb.validate.Validator[" 13 | ) || 14 | error.contains( 15 | "No given instance of type scalapb.validate.Validator[examplepb.options.NoValidator] was found" 16 | ), 17 | clues(error) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/ScalaHarness.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import scalapb.GeneratedFileObject 4 | import tests.harness.cases.bool.BoolProto 5 | import tests.harness.cases.bytes.BytesProto 6 | import tests.harness.cases.enums.EnumsProto 7 | import tests.harness.cases.kitchen_sink.KitchenSinkProto 8 | import tests.harness.cases.maps.MapsProto 9 | import tests.harness.cases.messages.MessagesProto 10 | import tests.harness.cases.numbers.NumbersProto 11 | import tests.harness.cases.oneofs.OneofsProto 12 | import tests.harness.cases.repeated.RepeatedProto 13 | import tests.harness.cases.strings.StringsProto 14 | import tests.harness.cases.wkt_any.WktAnyProto 15 | import tests.harness.cases.wkt_duration.WktDurationProto 16 | import tests.harness.cases.wkt_nested.WktNestedProto 17 | import tests.harness.cases.wkt_timestamp.WktTimestampProto 18 | import tests.harness.cases.wkt_wrappers.WktWrappersProto 19 | import tests.harness.cases.other_package.embed.EmbedProto 20 | import scalapb.GeneratedMessageCompanion 21 | import tests.harness.harness.TestCase 22 | import java.net.ServerSocket 23 | import tests.harness.harness.TestResult 24 | import scalapb.GeneratedMessage 25 | import scala.concurrent.Future 26 | import java.nio.file.Files 27 | import java.nio.file.Path 28 | import scala.sys.process.Process 29 | import scala.jdk.CollectionConverters._ 30 | import java.nio.file.attribute.PosixFilePermission 31 | import io.undertow.Undertow 32 | import io.undertow.server.HttpServerExchange 33 | import scala.concurrent.ExecutionContext.Implicits.global 34 | import io.undertow.server.handlers.BlockingHandler 35 | import java.net.Socket 36 | import java.net.ConnectException 37 | 38 | /** How this works? 39 | * 40 | * PGV test harness includes a go program (executor) that defines a protocol 41 | * for running the test cases against an arbitrary validator that can be 42 | * implemented in any programming language. 43 | * 44 | * For each test case, the executor launches a program (provided to it as a 45 | * binary), and sends an instance of a proto (wrapped in an Any) to the stdin 46 | * of that program. The program validates the instance and returns back a 47 | * "TestResult" containing the validity of the instance. The executor checks if 48 | * the program was correct and prints a test summary. 49 | * 50 | * Done naively, this approach would require invoking the JVM (or SBT) for each 51 | * of the 900+ test cases provided by PGV. To get around it, we start here an 52 | * HTTP server that implements the same protocol over HTTP. The executor is 53 | * provided with a shell script that calls `curl` to connect to the server. 54 | */ 55 | object ScalaHarness { 56 | val files = Seq( 57 | BoolProto, 58 | BytesProto, 59 | EnumsProto, 60 | KitchenSinkProto, 61 | MapsProto, 62 | MessagesProto, 63 | NumbersProto, 64 | OneofsProto, 65 | RepeatedProto, 66 | StringsProto, 67 | WktAnyProto, 68 | WktDurationProto, 69 | WktNestedProto, 70 | WktTimestampProto, 71 | WktWrappersProto, 72 | EmbedProto 73 | ) 74 | 75 | def allMessages( 76 | fileObject: GeneratedFileObject 77 | ): Seq[GeneratedMessageCompanion[_ <: GeneratedMessage]] = 78 | fileObject.messagesCompanions.flatMap(m => m +: allMessages(m)) 79 | 80 | def allMessages( 81 | cmp: GeneratedMessageCompanion[_] 82 | ): Seq[GeneratedMessageCompanion[_ <: GeneratedMessage]] = 83 | cmp.nestedMessagesCompanions.flatMap(m => m +: allMessages(m)) 84 | 85 | val messages = files.flatMap(allMessages) 86 | 87 | val typeMap: Map[String, GeneratedMessageCompanion[_ <: GeneratedMessage]] = 88 | messages.map { (cmp: GeneratedMessageCompanion[_ <: GeneratedMessage]) => 89 | (cmp.scalaDescriptor.fullName, cmp) 90 | }.toMap 91 | 92 | def processRequest(exchange: HttpServerExchange): Unit = { 93 | val testCase = TestCase.parseFrom(exchange.getInputStream()) 94 | val message = testCase.getMessage.typeUrl.substring(20) 95 | val cmp = typeMap 96 | .find(_._1 == message) 97 | .getOrElse(throw new RuntimeException(s"Could not find message $message")) 98 | ._2 99 | 100 | val inst = testCase.getMessage.unpack(cmp) 101 | 102 | val testResult = 103 | try { 104 | val klass = Class.forName( 105 | cmp.defaultInstance.getClass().getCanonicalName() + "Validator$" 106 | ) 107 | val vtor = klass 108 | .getField("MODULE$") 109 | .get(null) 110 | .asInstanceOf[Validator[GeneratedMessage]] 111 | vtor.validate(inst) match { 112 | case Success => TestResult(valid = true) 113 | case Failure(Nil) => // could be avoided with a NonEmptyList 114 | sys.error( 115 | "unexpected empty violation list" 116 | ) 117 | case Failure( 118 | ex :: _ // ignores multiple violations since pgv only supports one 119 | ) => 120 | val allowFailure = 121 | ex.reason == MapValidation.SPARSE_MAPS_NOT_SUPPORTED 122 | TestResult( 123 | valid = false, 124 | reasons = Seq(ex.reason), 125 | allowFailure = allowFailure 126 | ) 127 | } 128 | } catch { 129 | case ex: ClassNotFoundException => 130 | val isIgnored = 131 | cmp.scalaDescriptor.asProto.getOptions 132 | .extension( 133 | io.envoyproxy.pgv.validate.validate.ValidateProto.ignored 134 | ) 135 | .getOrElse(false) 136 | if (isIgnored) 137 | TestResult( 138 | valid = false, 139 | allowFailure = true, 140 | reasons = Seq("Validation not generated due to ignore option") 141 | ) 142 | else 143 | TestResult(valid = false, error = false, reasons = Seq(ex.toString)) 144 | } 145 | exchange 146 | .getResponseSender() 147 | .send(testResult.toByteString.asReadOnlyByteBuffer()) 148 | } 149 | 150 | def createScript(port: Int): Path = { 151 | val fileName = Files.createTempFile("spv-", ".sh") 152 | val os = Files.newOutputStream(fileName) 153 | os.write(s"""#!/usr/bin/env bash 154 | |set -e 155 | |curl --url http://localhost:$port/ -s --data-binary @- 156 | """.stripMargin.getBytes("UTF-8")) 157 | os.close() 158 | Files.setPosixFilePermissions( 159 | fileName, 160 | Set( 161 | PosixFilePermission.OWNER_EXECUTE, 162 | PosixFilePermission.OWNER_READ 163 | ).asJava 164 | ) 165 | fileName 166 | } 167 | 168 | val port = { 169 | val ss = new ServerSocket(0) 170 | try ss.getLocalPort() 171 | finally ss.close() 172 | } 173 | 174 | def waitForServer(port: Int, timeoutMsec: Int): Unit = { 175 | var timeLeft = timeoutMsec 176 | var done = false 177 | while (!done) 178 | try { 179 | val c = new Socket("localhost", port) 180 | c.close() 181 | done = true 182 | } catch { 183 | case _: ConnectException if timeLeft >= 0 => 184 | Thread.sleep(100) 185 | timeLeft -= 100 186 | } 187 | } 188 | 189 | def main(args: Array[String]): Unit = { 190 | val server = Undertow.builder 191 | .addHttpListener(port, "localhost") 192 | .setHandler(new BlockingHandler(processRequest(_))) 193 | .build 194 | Future(server.start()) 195 | val script = createScript(port) 196 | waitForServer(port, 10000) 197 | val status = 198 | try 199 | Process( 200 | "./executor.exe", 201 | Seq( 202 | "-external_harness", 203 | script.toString() 204 | ) 205 | ).! 206 | finally { 207 | Files.delete(script) 208 | server.stop() 209 | } 210 | 211 | sys.exit(status) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/SkipSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | class SkipSpec extends munit.FunSuite with ValidationHelpers { 4 | test("third_party validators dont exist") { 5 | val error = 6 | compileErrors( 7 | "implicitly[scalapb.validate.Validator[third_party.third_party.CantChangeThisMessage]]" 8 | ) 9 | 10 | assert( 11 | error.contains( 12 | "could not find implicit value for parameter e: scalapb.validate.Validator[third_party.third_party.CantChangeThisMessage]" 13 | ) || 14 | error.contains( 15 | "No given instance of type scalapb.validate.Validator[third_party.third_party" 16 | ), 17 | clues(error) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/ValidationHelpers.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | trait ValidationHelpers { 4 | this: munit.FunSuite => 5 | 6 | def assertFailure[T](r: Result, expected: List[(String, AnyRef)])(implicit 7 | loc: munit.Location 8 | ) = 9 | r match { 10 | case Success => fail("expected a Failure, but was a Success") 11 | case Failure(violations) => 12 | val fieldAndValues = violations.map { v => 13 | v.field -> v.value 14 | } 15 | assertEquals(fieldAndValues, expected) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/ValidatorSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate 2 | 3 | import examplepb.example.Person 4 | import examplepb2.required.{Person => Person2} 5 | import examplepb3.optional.{ 6 | Person => Person3, 7 | RequiredMessage => RequiredMessage3 8 | } 9 | 10 | class ValidatorSpec extends munit.FunSuite with ValidationHelpers { 11 | val testPerson = Person( 12 | id = 1000, 13 | email = "foo@bar.com", 14 | name = "Protocol Buffer", 15 | home = Some(Person.Location(lat = 0, lng = 0)), 16 | age = 35 17 | ) 18 | test("Person empty") { 19 | assertEquals( 20 | Validator[Person].validate(testPerson), 21 | Success 22 | ) 23 | } 24 | test("Person invalid email") { 25 | assertFailure( 26 | Validator[Person].validate(testPerson.copy(email = "not an email")), 27 | ("Person.email", "\"not an email\"") :: Nil 28 | ) 29 | } 30 | test("Person invalid email and age") { 31 | assertFailure( 32 | Validator[Person] 33 | .validate(testPerson.copy(email = "not an email", age = -1)), 34 | ("Person.email", "\"not an email\"") :: ("Person.age", Int.box(-1)) :: Nil 35 | ) 36 | } 37 | test("Person invalid location lat") { 38 | assertFailure( 39 | Validator[Person] 40 | .validate( 41 | testPerson.copy(home = Some(Person.Location(lat = 100, lng = 0))) 42 | ), 43 | ("Person.Location.lat", Double.box(100)) :: Nil 44 | ) 45 | } 46 | 47 | // See issue #21 48 | test("Required message is validated") { 49 | assertFailure( 50 | Validator[Person2] 51 | .validate( 52 | Person2(email = "foo@foo.com", home = Person2.Location(91, 0)) 53 | ), 54 | ("Person.Location.lat", Double.box(91.0)) :: Nil 55 | ) 56 | 57 | } 58 | 59 | test("Optional field presence is recognized and validated") { 60 | assertFailure( 61 | Validator[Person3] 62 | .validate( 63 | Person3(name = None, age = Some(1), height = None) 64 | ), 65 | ("Person.name", "None") :: Nil 66 | ) 67 | } 68 | 69 | test("Optional field presence is recognized and validated") { 70 | assertFailure( 71 | Validator[RequiredMessage3] 72 | .validate( 73 | RequiredMessage3() 74 | ), 75 | ("RequiredMessage.person", "None") :: Nil 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/cats/CatsTypesSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import cats.data.{NonEmptyList, NonEmptyMap, NonEmptySet} 4 | import e2e.cats.types.{ 5 | NonEmptyTypes, 6 | NonEmptyTypesTesting, 7 | NonEmptyTypesWithSubRules 8 | } 9 | import scalapb.validate.Success 10 | import scalapb.validate.Validator 11 | import scalapb.validate.ValidationHelpers 12 | import scalapb.validate.ValidationException 13 | import scalapb.json4s.JsonFormat 14 | import e2e.cats.alltypes.instances 15 | import scalapb.GeneratedMessage 16 | import scalapb.GeneratedMessageCompanion 17 | 18 | class CatsTypesSpec extends munit.FunSuite with ValidationHelpers { 19 | val nonEmptyTypes = NonEmptyTypes( 20 | nonEmptySet = NonEmptySet.of("foo", "bar"), 21 | nonEmptyList = NonEmptyList.of("bar", "baz"), 22 | nonEmptyMap = NonEmptyMap.of(3 -> 4) 23 | ) 24 | 25 | val nonEmptyTypesTesting = NonEmptyTypesTesting( 26 | nonEmptySet = Seq("foo", "bar"), 27 | nonEmptyList = Seq("baz"), 28 | nonEmptyMap = Map(3 -> 4) 29 | ) 30 | 31 | test("NonEmptyTypes serialize and parse successfully") { 32 | assertEquals( 33 | NonEmptyTypes.parseFrom(nonEmptyTypes.toByteArray), 34 | nonEmptyTypes 35 | ) 36 | assertEquals( 37 | NonEmptyTypes.fromAscii(nonEmptyTypes.toProtoString), 38 | nonEmptyTypes 39 | ) 40 | assertEquals( 41 | Validator[NonEmptyTypes].validate(nonEmptyTypes).isSuccess, 42 | true 43 | ) 44 | } 45 | 46 | test("NonEmptyTypes fails to construct if invalid") { 47 | intercept[ValidationException] { 48 | nonEmptyTypes.copy(foo = "verylongstring") 49 | } 50 | } 51 | 52 | test("NonEmptyTypes fail when input is empty") { 53 | interceptMessage[ValidationException]( 54 | "Could not build an empty NonEmptySet" 55 | )(NonEmptyTypes.parseFrom(Array[Byte]())) 56 | } 57 | 58 | val subrules = NonEmptyTypesWithSubRules( 59 | nonEmptySet = NonEmptySet.of("fooba", "gooma"), 60 | nonEmptyList = NonEmptyList.of("fooba", "gooma"), 61 | nonEmptyMap = NonEmptyMap.of(4 -> 5) 62 | ) 63 | 64 | test("NonEmptyTypesWithSubRules should validate correctly") { 65 | assertEquals( 66 | Validator[NonEmptyTypesWithSubRules].validate(subrules), 67 | Success 68 | ) 69 | } 70 | 71 | test("NonEmptyTypesWithSubRules should fail member validation") { 72 | val invalid1 = 73 | subrules.update( 74 | _.nonEmptySet := NonEmptySet.of("foo") 75 | ) // str length is not 5 76 | val invalid2 = 77 | subrules.update( 78 | _.nonEmptyList := NonEmptyList.of("foo") 79 | ) // str length is not 5 80 | val invalid3 = 81 | subrules.update(_.nonEmptyMap := NonEmptyMap.of(1 -> 12)) // key not gte 4 82 | val invalid4 = 83 | subrules.update( 84 | _.nonEmptyMap := NonEmptyMap.of(4 -> 4) 85 | ) // value not gte 5 86 | 87 | assertFailure( 88 | Validator[NonEmptyTypesWithSubRules].validate(invalid1), 89 | List( 90 | ("NonEmptyTypesWithSubRules.non_empty_set", "\"foo\"") 91 | ) 92 | ) 93 | assertFailure( 94 | Validator[NonEmptyTypesWithSubRules].validate(invalid2), 95 | List( 96 | ("NonEmptyTypesWithSubRules.non_empty_list", "\"foo\"") 97 | ) 98 | ) 99 | assertFailure( 100 | Validator[NonEmptyTypesWithSubRules].validate(invalid3), 101 | List( 102 | ("NonEmptyTypesWithSubRules.non_empty_map", Int.box(1)) 103 | ) 104 | ) 105 | assertFailure( 106 | Validator[NonEmptyTypesWithSubRules].validate(invalid4), 107 | List( 108 | ("NonEmptyTypesWithSubRules.non_empty_map", Int.box(4)) 109 | ) 110 | ) 111 | } 112 | 113 | test("cat types are json-serializable") { 114 | assertEquals( 115 | JsonFormat.fromJsonString[NonEmptyTypes]( 116 | JsonFormat.toJsonString(nonEmptyTypes) 117 | ), 118 | nonEmptyTypes 119 | ) 120 | } 121 | 122 | test("Throws exception for empty list in json parsing") { 123 | val j = """ 124 | {"nonEmptySet":["bar","foo"],"nonEmptyList":[],"nonEmptyMap":{"3":4}} 125 | """ 126 | interceptMessage[ValidationException]( 127 | "Could not build an empty NonEmptyList" 128 | ) { 129 | JsonFormat.fromJsonString[NonEmptyTypes](j) 130 | } 131 | } 132 | 133 | test("Throws exception when non-empty set has duplicate elements") { 134 | val n = nonEmptyTypesTesting.withNonEmptySet(Seq("foo", "foo")) 135 | interceptMessage[ValidationException]( 136 | "Got duplicate elements for NonEmptySet" 137 | )(NonEmptyTypes.parseFrom(n.toByteArray)) 138 | } 139 | 140 | test("Throws exception when set has duplicate elements") { 141 | val n = nonEmptyTypesTesting.withSet(Seq("foo", "foo")) 142 | interceptMessage[ValidationException]( 143 | "Got duplicate elements for Set" 144 | )(NonEmptyTypes.parseFrom(n.toByteArray)) 145 | } 146 | 147 | test("all instances serialize and parse to binary") { 148 | instances.all.foreach { p => 149 | assertEquals( 150 | p.companion.parseFrom(p.toByteArray).asInstanceOf[GeneratedMessage], 151 | p 152 | ) 153 | } 154 | } 155 | 156 | test("all instances serialize and parse to json") { 157 | instances.all.foreach { p => 158 | assertEquals( 159 | JsonFormat 160 | .fromJsonString[GeneratedMessage](JsonFormat.toJsonString(p))( 161 | p.companion 162 | .asInstanceOf[GeneratedMessageCompanion[GeneratedMessage]] 163 | ) 164 | .asInstanceOf[GeneratedMessage], 165 | p 166 | ) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/cats/ExcludedSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import e2e.cats.excluded.Excluded 4 | 5 | class ExcludedSpec extends munit.FunSuite { 6 | // We are testing that nonEmptySet has not been transformed into 7 | // cats.data.NonEmptySet since the proto file is excluded. 8 | val m = Excluded(nonEmptySet = List()) 9 | } 10 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/cats/NonEmptySpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.cats 2 | 3 | import _root_.cats.data.NonEmptyChain 4 | import _root_.cats.data.NonEmptySeq 5 | import _root_.cats.data.NonEmptyVector 6 | import e2e.cats.non_empty_seq.NonEmptySeqTest 7 | import e2e.cats.non_empty_vector.NonEmptyVectorTest 8 | import e2e.cats.non_empty_chain.NonEmptyChainTest 9 | import scalapb.validate.ValidationHelpers 10 | import scalapb.validate.Validator 11 | 12 | class NonEmptySpec extends munit.FunSuite with ValidationHelpers { 13 | test("NonEmptySeq serialize and parse successfully") { 14 | val x = NonEmptySeqTest.of(NonEmptySeq.of(1, 2, 3)) 15 | assertEquals(NonEmptySeqTest.parseFrom(x.toByteArray), x) 16 | assertEquals(NonEmptySeqTest.fromAscii(x.toProtoString), x) 17 | assert(Validator[NonEmptySeqTest].validate(x).isSuccess) 18 | } 19 | 20 | test("NonEmptyVector serialize and parse successfully") { 21 | val x = NonEmptyVectorTest.of(NonEmptyVector.of("a", "b", "c")) 22 | assertEquals(NonEmptyVectorTest.parseFrom(x.toByteArray), x) 23 | assertEquals(NonEmptyVectorTest.fromAscii(x.toProtoString), x) 24 | assert(Validator[NonEmptyVectorTest].validate(x).isSuccess) 25 | } 26 | 27 | test("NonEmptyChain serialize and parse successfully") { 28 | val x = NonEmptyChainTest.of(NonEmptyChain.of(true, false)) 29 | assertEquals(NonEmptyChainTest.parseFrom(x.toByteArray), x) 30 | assertEquals(NonEmptyChainTest.fromAscii(x.toProtoString), x) 31 | assert(Validator[NonEmptyChainTest].validate(x).isSuccess) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/transforms/OrderSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.transforms 2 | 3 | import scalapb.validate.ValidationHelpers 4 | import scalapb.transforms.PositiveInt 5 | 6 | import scalapb.transforms.order.order2.order3.order3.TestMessage 7 | 8 | class OrderSpec extends munit.FunSuite with ValidationHelpers { 9 | val m = TestMessage(bam = Some(PositiveInt(17))) 10 | } 11 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/transforms/RequiredSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.transforms 2 | 3 | import com.google.protobuf.InvalidProtocolBufferException 4 | import scalapb.transforms.required.{Container, RequiredMsg} 5 | import scalapb.json4s.JsonFormat 6 | 7 | class RequiredSpec extends munit.FunSuite { 8 | 9 | test("Container should have unboxed requiredMsg") { 10 | val c = Container(requiredMsg = RequiredMsg()) // <-- not in an option 11 | assertEquals(Container.parseFrom(c.toByteArray), c) 12 | } 13 | 14 | test("Container should fail parsing empty message") { 15 | interceptMessage[InvalidProtocolBufferException]( 16 | "Message missing required fields." 17 | ) { 18 | Container.parseFrom(Array.empty[Byte]) 19 | } 20 | } 21 | 22 | test("Container should serialize from/to json") { 23 | val c = Container(requiredMsg = RequiredMsg()) // <-- not in an option 24 | assertEquals( 25 | JsonFormat.fromJsonString[Container]( 26 | JsonFormat.toJsonString(c) 27 | ), 28 | c 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /e2e/src/test/scala/scalapb/validate/transforms/TransformSpec.scala: -------------------------------------------------------------------------------- 1 | package scalapb.validate.transforms 2 | 3 | import scalapb.validate.ValidationException 4 | import scalapb.validate.ValidationHelpers 5 | import scalapb.transforms.field.{MyTestMessage, MyTestMessageWithNonEmpty} 6 | import scalapb.transforms.PositiveInt 7 | import scalapb.validate.{Success, Validator} 8 | import cats.data.NonEmptyList 9 | import cats.data.NonEmptyMap 10 | import cats.data.NonEmptySet 11 | import scalapb.transforms.MyCustomType 12 | 13 | class TransformsSpec extends munit.FunSuite with ValidationHelpers { 14 | val inst = MyTestMessage(optPosNum = PositiveInt(4)) 15 | 16 | test("MyTestMessage default constructor fails") { 17 | interceptMessage[AssertionError]("assertion failed")(MyTestMessage()) 18 | } 19 | 20 | val msg = MyTestMessage( 21 | optPosNum = PositiveInt(17), 22 | repPosNum = Seq(PositiveInt(3)), 23 | setPosNum = Set(PositiveInt(4)), 24 | Map(PositiveInt(4) -> PositiveInt(6)) 25 | ) 26 | 27 | test("MyTestMessage has correct types") { 28 | assertEquals(MyTestMessage.parseFrom(msg.toByteArray), msg) 29 | assertEquals(Validator[MyTestMessage].validate(msg), Success) 30 | } 31 | 32 | test("MyTestMessage will not instantiate invalid") { 33 | assert(intercept[ValidationException] { 34 | msg.copy(foo = -20) 35 | }.getMessage().contains("MyTestMessage.foo: -20 must be greater than -5")) 36 | } 37 | 38 | test("MyTestMessageWithNonEmpty has correct types") { 39 | implicit val t = cats.kernel.Order.fromOrdering(PositiveInt.ordering) 40 | val msg = MyTestMessageWithNonEmpty( 41 | optPosNum = PositiveInt(17), 42 | setPosNum = NonEmptySet.of(PositiveInt(9)), 43 | repPosNum = NonEmptyList.of(PositiveInt(3)), 44 | mapPosNums = NonEmptyMap.of(PositiveInt(4) -> PositiveInt(6)), 45 | mapMsg = NonEmptyMap.of(PositiveInt(4) -> MyCustomType("boo")) 46 | ) 47 | assertEquals(MyTestMessageWithNonEmpty.parseFrom(msg.toByteArray), msg) 48 | assertEquals(Validator[MyTestMessageWithNonEmpty].validate(msg), Success) 49 | intercept[ValidationException] { 50 | msg.withMapMsg( 51 | NonEmptyMap.of(PositiveInt(4) -> MyCustomType("boom")) // must be boo 52 | ) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/build.sbt: -------------------------------------------------------------------------------- 1 | name := "validate-example" 2 | 3 | scalaVersion := "2.13.2" 4 | 5 | ThisBuild / resolvers += Resolver.sonatypeRepo("snapshots") 6 | 7 | ThisBuild / scalacOptions ++= Seq("-Xfatal-warnings", "-Xlint") 8 | 9 | Compile / PB.targets := Seq( 10 | scalapb.gen() -> (Compile / sourceManaged).value / "scalapb", 11 | scalapb.validate.gen() -> (Compile / sourceManaged).value / "scalapb" 12 | ) 13 | 14 | libraryDependencies ++= Seq( 15 | "com.thesamet.scalapb" %% "scalapb-validate-core" % scalapb.validate.compiler.BuildInfo.version % "protobuf" 16 | ) 17 | -------------------------------------------------------------------------------- /example/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.13 2 | -------------------------------------------------------------------------------- /example/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / resolvers += Resolver.sonatypeRepo("snapshots") 2 | 3 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.34") 4 | 5 | libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.10.7" 6 | 7 | val validateVersion = "0.1.1" 8 | 9 | libraryDependencies += "com.thesamet.scalapb" %% "scalapb-validate-codegen" % validateVersion 10 | -------------------------------------------------------------------------------- /example/src/main/protobuf/proto3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package myexample; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | import "validate/validate.proto"; 7 | 8 | message Person { 9 | int32 age = 2 [(validate.rules).int32 = {gte: 18}]; 10 | google.protobuf.StringValue name = 1 [(validate.rules).message.required=true]; 11 | } 12 | -------------------------------------------------------------------------------- /example/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package myexample 2 | 3 | import myexample.proto3.{Person, PersonValidator} 4 | 5 | object Main { 6 | def main(args: Array[String]): Unit = { 7 | assert( 8 | PersonValidator 9 | .validate( 10 | Person(age = 12, name = Some("John")) 11 | ) 12 | .toFailure 13 | .get 14 | .violation 15 | .getMessage() == "age: 12 must be greater than or equal to 18 - Got 12" 16 | ) 17 | 18 | assert( 19 | PersonValidator 20 | .validate( 21 | Person(age = 20, name = None) 22 | ) 23 | .toFailure 24 | .get 25 | .violation 26 | .getMessage() == "name: is required - Got None" 27 | ) 28 | 29 | assert( 30 | PersonValidator 31 | .validate( 32 | Person(age = 20, name = Some("John")) 33 | ) 34 | .isSuccess 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /get_bazelisk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | if [[ -f ./bazelisk ]]; then 4 | echo bazelisk exists 5 | else 6 | if [[ $OSTYPE =~ "darwin" ]]; then 7 | export URL=https://github.com/bazelbuild/bazelisk/releases/download/v1.7.5/bazelisk-darwin-amd64 8 | else 9 | export URL=https://github.com/bazelbuild/bazelisk/releases/download/v1.7.5/bazelisk-linux-amd64 10 | fi 11 | 12 | curl -L $URL -o bazel 13 | chmod +x ./bazel 14 | export PATH=$PATH:$PWD 15 | fi 16 | -------------------------------------------------------------------------------- /make_harness.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | curl -L https://github.com/thesamet/protoc-gen-validate/releases/download/v0.6.1-mod/executor-0.6.1-linux-x86_64.exe -o executor.exe 4 | chmod 0755 ./executor.exe 5 | -------------------------------------------------------------------------------- /project/Settings.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Settings { 5 | val stdOptions = 6 | Seq("-deprecation", "-encoding", "UTF-8", "-feature", "-unchecked") 7 | 8 | val std2xOptions = Seq( 9 | "-language:higherKinds", 10 | "-language:existentials", 11 | "-explaintypes", 12 | "-Yrangepos", 13 | "-Xlint:_,-missing-interpolator,-type-parameter-shadow", 14 | "-Ywarn-numeric-widen", 15 | "-Ywarn-value-discard" 16 | ) 17 | 18 | def extraOptions(scalaVersion: String) = 19 | CrossVersion.partialVersion(scalaVersion) match { 20 | case Some((2, 13)) => 21 | Seq( 22 | "-Ywarn-unused:params,-implicits" 23 | ) ++ std2xOptions 24 | case Some((2, 12)) => 25 | Seq( 26 | "-opt-warnings", 27 | "-Ywarn-extra-implicit", 28 | "-Ywarn-unused:_,imports", 29 | "-Ywarn-unused:imports", 30 | "-Ypartial-unification", 31 | "-Yno-adapted-args", 32 | "-Ywarn-inaccessible", 33 | "-Ywarn-infer-any", 34 | "-Ywarn-nullary-override", 35 | "-Ywarn-nullary-unit", 36 | "-Ywarn-unused:params,-implicits", 37 | "-Xfuture", 38 | "-Xsource:2.13", 39 | "-Xmax-classfile-name", 40 | "242" 41 | ) ++ std2xOptions 42 | case _ => Seq() 43 | } 44 | 45 | def stdSettings = 46 | Seq( 47 | scalacOptions := stdOptions ++ extraOptions(scalaVersion.value) 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /project/TestProtosGenerator.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbt.Keys._ 3 | import sbtprotoc.ProtocPlugin.autoImport.PB 4 | 5 | object TestProtosGenerator { 6 | val protoBase = 7 | """syntax = "proto2"; 8 | | 9 | |// DO NOT EDIT. Generated by project/TestProtosGenerator.scala 10 | | 11 | |package e2e.cats; 12 | | 13 | |import "validate/validate.proto"; 14 | |import "cats/types.proto"; 15 | |""".stripMargin 16 | 17 | case class Vars(typ: String, mapKey: Boolean = true, elem: String) 18 | 19 | def template(v: Vars) = 20 | s"""message CatsTest${v.typ.capitalize} { 21 | | repeated ${v.typ} set = 1 [(validate.rules).repeated = { unique: true }]; 22 | | repeated ${v.typ} non_empty_set = 2 [(validate.rules).repeated = { min_items: 1, unique: true }]; 23 | | repeated ${v.typ} non_empty_list = 3 [(validate.rules).repeated = { min_items: 1 }]; 24 | | ${if (!v.mapKey) "// " 25 | else 26 | ""}map<${v.typ}, int32> non_empty_map_key = 4 [(validate.rules).map = { min_pairs: 1 }]; 27 | | map non_empty_map_value = 5 [(validate.rules).map = { min_pairs: 1 }]; 28 | |}""".stripMargin 29 | 30 | val types = Seq( 31 | Vars("int32", elem = "1"), 32 | Vars("int64", elem = "2L"), 33 | Vars("uint32", elem = "3"), 34 | Vars("uint64", elem = "4L"), 35 | Vars("sfixed32", elem = "5"), 36 | Vars("sfixed64", elem = "6L"), 37 | Vars("string", elem = "\"str\""), 38 | Vars( 39 | "bytes", 40 | mapKey = false, 41 | elem = "com.google.protobuf.ByteString.copyFrom(Array[Byte]())" 42 | ), 43 | Vars("double", mapKey = false, elem = "13.21"), 44 | Vars("float", mapKey = false, elem = "3.14f"), 45 | Vars("Color", mapKey = false, elem = "e2e.cats.types.Color.RED"), 46 | Vars("SubMsg", mapKey = false, elem = "e2e.cats.types.SubMsg()") 47 | ) 48 | 49 | def makeInstance(v: Vars): String = 50 | s"""| val ${v.typ} = CatsTest${v.typ.capitalize}( 51 | | set=Set(${v.elem}), 52 | | nonEmptySet=NonEmptySet.of(${v.elem}), 53 | | nonEmptyList=NonEmptyList.of(${v.elem}),${if (v.mapKey) 54 | s""" 55 | | nonEmptyMapKey=NonEmptyMap.of(${v.elem} -> 17),""" 56 | else ""} 57 | | nonEmptyMapValue=NonEmptyMap.of(17 -> ${v.elem}) 58 | | )""" 59 | 60 | val instances = 61 | s"""|package e2e.cats.alltypes 62 | | 63 | |import cats.data._ 64 | | 65 | |object instances { 66 | ${types.map(makeInstance).mkString("\n")} 67 | | val all: Seq[scalapb.GeneratedMessage] = Seq(${types 68 | .map(_.typ) 69 | .mkString(", ")}) 70 | |}""".stripMargin 71 | 72 | val generateAllTypesProto = taskKey[Seq[File]]("Generates alltypes.proto") 73 | 74 | def generateAllTypesProtoSettings = Seq( 75 | generateAllTypesProto := { 76 | val protoContent = protoBase + types.map(template).mkString("\n") 77 | val protoFile = 78 | (Compile / sourceManaged).value / "protobuf" / "alltypes.proto" 79 | IO.write(protoFile, protoContent) 80 | Seq(protoFile) 81 | }, 82 | (Compile / sourceGenerators) += Def.task { 83 | val scalaFile = 84 | (Compile / sourceManaged).value / "alltypes" / "instances.scala" 85 | IO.write(scalaFile, instances) 86 | Seq(scalaFile) 87 | }, 88 | Compile / PB.generate := 89 | (Compile / PB.generate dependsOn TestProtosGenerator.generateAllTypesProto).value 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.8") 2 | 3 | libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.11.17" 4 | 5 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 6 | 7 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.3") 8 | 9 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 10 | 11 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") 12 | 13 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.11.0") 14 | 15 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") 16 | 17 | addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1") 18 | 19 | addSbtPlugin("com.thesamet" % "sbt-protoc-gen-project" % "0.1.8") 20 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import { 2 | config = { 3 | packageOverrides = pkgs: { 4 | sbt = pkgs.sbt.override { jre = pkgs.openjdk11; }; 5 | }; 6 | }; 7 | }} : 8 | pkgs.mkShell { 9 | buildInputs = [ 10 | pkgs.sbt 11 | pkgs.openjdk11 12 | pkgs.nodejs 13 | pkgs.go 14 | 15 | # keep this line if you use bash 16 | pkgs.bashInteractive 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /sync_pgv_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | TAG=0.6.3-java 4 | 5 | curl -L https://github.com/envoyproxy/protoc-gen-validate/archive/v$TAG.tar.gz | \ 6 | tar xvz --strip-components=1 -C e2e/src/main/protobuf protoc-gen-validate-$TAG/tests 7 | 8 | --------------------------------------------------------------------------------