diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 2a60a6887..000000000 --- a/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -[*] -charset = utf-8 - -[*.java] -ij_java_class_count_to_use_import_on_demand = 999 -ij_java_use_single_class_imports = true diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7b5280256..c9d495979 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -175,6 +175,6 @@ working together to make `protovalidate` the best it can be. [file-bug]: https://github.com/bufbuild/protovalidate-java/issues/new?assignees=&labels=Bug&template=bug_report.md&title=%5BBUG%5D -[file-feature-request]: https://github.com/bufbuild/protovalidate-java/issues/new?assignees=&labels=Feature&template=feature_request.md&title=%5BFeature+Request%5D +[file-feature-request]: https://github.com/bufbuild/protovalidate=java/issues/new?assignees=&labels=Feature&template=feature_request.md&title=%5BFeature+Request%5D [cel-spec]: https://github.com/google/cel-spec diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 49e66ebf8..23779eb76 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -36,6 +36,7 @@ assignees: '' - **Version**: - **Compiler/Toolchain**: - **Protobuf Compiler & Version**: +- **Protoc-gen-validate Version**: - **Protovalidate Version**: ## Possible Solution diff --git a/.github/buf-logo.svg b/.github/buf-logo.svg index 2a24f001f..35bcafdee 100644 --- a/.github/buf-logo.svg +++ b/.github/buf-logo.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5b5af4145..10ef83118 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,3 @@ updates: directory: "/" schedule: interval: "weekly" - diff --git a/.github/release.yml b/.github/release.yml deleted file mode 100644 index 97b937216..000000000 --- a/.github/release.yml +++ /dev/null @@ -1,6 +0,0 @@ -changelog: - exclude: - labels: - - ignore-for-release - authors: - - dependabot[bot] diff --git a/.github/workflows/add-to-project.yaml b/.github/workflows/add-to-project.yaml deleted file mode 100644 index e3c0b4225..000000000 --- a/.github/workflows/add-to-project.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: Add issues and PRs to project - -on: - issues: - types: - - opened - - reopened - - transferred - pull_request_target: - types: - - opened - - reopened - issue_comment: - types: - - created - -jobs: - call-workflow-add-to-project: - name: Call workflow to add issue to project - uses: bufbuild/base-workflows/.github/workflows/add-to-project.yaml@main - secrets: inherit diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d04d969ac..02006c8b1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,27 +16,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Cache Go Modules - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/checkout@v3 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-gomod-ci-${{ hashFiles('gradle.properties', 'gradle/libs.versions.toml') }} - restore-keys: - ${{ runner.os }}-gomod-ci- - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + fetch-depth: 1 + - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '21' + java-version: '17' cache: 'gradle' - - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 # v1.5.0 - with: - setup_only: true - token: ${{ secrets.BUF_TOKEN }} - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - name: Lint run: make lint - name: Generate diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml index e805a7edc..121b27af9 100644 --- a/.github/workflows/conformance.yaml +++ b/.github/workflows/conformance.yaml @@ -16,26 +16,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Cache Go Modules - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/checkout@v3 with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-gomod-conformance-${{ hashFiles('gradle.properties', 'gradle/libs.versions.toml') }} - restore-keys: - ${{ runner.os }}-gomod-conformance- - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + fetch-depth: 1 + - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '21' + java-version: '17' cache: 'gradle' - - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 # v1.5.0 - with: - setup_only: true - token: ${{ secrets.BUF_TOKEN }} - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - name: Test conformance - run: make conformance + run: make conformance \ No newline at end of file diff --git a/.github/workflows/pr-hygiene.yaml b/.github/workflows/pr-hygiene.yaml index f75d0bc76..29e48ab30 100644 --- a/.github/workflows/pr-hygiene.yaml +++ b/.github/workflows/pr-hygiene.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Lint title - uses: morrisoncole/pr-lint-action@51f3cfabaf5d46f94e54524214e45685f0401b2a # v1.7.1 + uses: morrisoncole/pr-lint-action@v1.7.0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" # https://regex101.com/r/I6oK5v/1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5f9fa5aec..eb5e1b939 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,39 +1,25 @@ name: Gradle Publish on: - push: - tags: - - v* - workflow_dispatch: {} # support manual runs -permissions: - contents: write + release: + types: [created] jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Cache Go Modules - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-gomod-ci-${{ hashFiles('gradle.properties', 'gradle/libs.versions.toml') }} - restore-keys: - ${{ runner.os }}-gomod-ci- - - name: Set up JDK - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + - uses: actions/checkout@v3 + + - name: Set up JDK 8 + uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '21' - - uses: bufbuild/buf-action@8c6a16e16f12ba20b6470afa9c2ba9b5ba8c97c3 # v1.5.0 - with: - setup_only: true - token: ${{ secrets.BUF_TOKEN }} + java-version: '8' + - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + uses: gradle/wrapper-validation-action@v1 + - name: Configure GPG signing & publish env: GPG_KEY: ${{ secrets.GPG_KEY }} diff --git a/.gitignore b/.gitignore index e6ddc561b..603c3b5fd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,7 @@ *.iml /.idea /.tmp -/bin/ # Cache of project /.gradletasknamecache # Ignore Gradle GUI config -/gradle-app.setting \ No newline at end of file +/gradle-app.setting diff --git a/LICENSE b/LICENSE index 3e1d480e1..dbb493206 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023-2025 Buf Technologies, Inc. + Copyright 2023 Buf Technologies, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile index 2aa35c07d..23bca38ad 100644 --- a/Makefile +++ b/Makefile @@ -6,18 +6,31 @@ SHELL := bash MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-print-directory -GRADLE ?= ./gradlew +BIN := .tmp/bin +COPYRIGHT_YEARS := 2023 +LICENSE_IGNORE := +JAVA_VERSION = 20 +JAVAC = javac +JAVA = java +GO ?= go +ARGS ?= --strict_message --streaming_host="http://localhost:1235" +JAVA_COMPILE_OPTIONS = --enable-preview --release $(JAVA_VERSION) +JAVA_OPTIONS = --enable-preview +PROTOVALIDATE_VERSION ?= 7a97b32c49d7e9caec91e2fe104cc414d494f4e8 +JAVA_MAIN_CLASS = build.buf.protovalidate +JAVA_SOURCES = $(wildcard src/main/java/**/**/**/*.java, src/main/java/**/**/*.java) +JAVA_CLASSES = $(patsubst src/main/java/%.java, target/classes/%.class, $(JAVA_SOURCES)) .PHONY: all all: lint generate build docs conformance ## Run all tests and lint (default) .PHONY: build build: ## Build the entire project. - $(GRADLE) build + ./gradlew build .PHONY: docs docs: ## Build javadocs for the project. - $(GRADLE) javadoc + ./gradlew javadoc .PHONY: checkgenerate checkgenerate: generate ## Checks if `make generate` produces a diff. @@ -26,37 +39,58 @@ checkgenerate: generate ## Checks if `make generate` produces a diff. .PHONY: clean clean: ## Delete intermediate build artifacts - $(GRADLE) clean + @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs + git clean -Xdf .PHONY: conformance -conformance: ## Execute conformance tests with native rule evaluators enabled and disabled. - ENABLE_NATIVE_RULES=true $(GRADLE) conformance:conformance - ENABLE_NATIVE_RULES=false $(GRADLE) conformance:conformance +conformance: build $(BIN)/protovalidate-conformance ## Execute conformance tests. + ./conformance/conformance.sh & + $(BIN)/protovalidate-conformance $(ARGS) + pkill -f "java" + +.PHONY: generate-license +generate-license: $(BIN)/license-header ## Generates license headers for all source files. + $(BIN)/license-header \ + --license-type apache \ + --copyright-holder "Buf Technologies, Inc." \ + --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) .PHONY: help -help: ## Describe useful make targets +help: ## Describe useful make targets @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}' .PHONY: generate -generate: ## Regenerate code and license headers - $(GRADLE) generate +generate: generate-license ## Regenerate code and license headers .PHONY: lint lint: ## Lint code - $(GRADLE) spotlessCheck + ./gradlew spotlessCheck .PHONY: lintfix lintfix: ## Applies the lint changes. - $(GRADLE) spotlessApply + ./gradlew spotlessApply .PHONY: release -release: ## Upload artifacts to Maven Central. - $(GRADLE) --info publishAndReleaseToMavenCentral --stacktrace --no-daemon --no-parallel --no-configuration-cache +release: ## Upload artifacts to Sonatype Nexus. + ./gradlew --info publish --stacktrace --no-daemon --no-parallel + ./gradlew --info closeAndReleaseRepository .PHONY: releaselocal releaselocal: ## Release artifacts to local maven repository. - $(GRADLE) --info publishToMavenLocal + ./gradlew --info publishToMavenLocal .PHONY: test test: ## Run all tests. - $(GRADLE) test + ./gradlew test + +$(BIN): + @mkdir -p $(BIN) + +$(BIN)/license-header: $(BIN) Makefile + GOBIN=$(abspath $(@D)) $(GO) install \ + github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@latest + +$(BIN)/protovalidate-conformance: $(BIN) Makefile + GOBIN=$(abspath $(BIN)) $(GO) install \ + github.com/bufbuild/protovalidate/tools/protovalidate-conformance@$(PROTOVALIDATE_VERSION) + diff --git a/README.md b/README.md index 7a535db8b..c9f324f91 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,33 @@ -[![The Buf logo](.github/buf-logo.svg)][buf] - -# protovalidate-java +# [![The Buf logo](.github/buf-logo.svg)][buf] protovalidate-java [![CI](https://github.com/bufbuild/protovalidate-java/actions/workflows/ci.yaml/badge.svg)](https://github.com/bufbuild/protovalidate-java/actions/workflows/ci.yaml) [![Conformance](https://github.com/bufbuild/protovalidate-java/actions/workflows/conformance.yaml/badge.svg)](https://github.com/bufbuild/protovalidate-java/actions/workflows/conformance.yaml) [![BSR](https://img.shields.io/badge/BSR-Module-0C65EC)][buf-mod] -[Protovalidate][protovalidate] is the semantic validation library for Protobuf. It provides standard annotations to validate common rules on messages and fields, as well as the ability to use [CEL][cel] to write custom rules. It's the next generation of [protoc-gen-validate][protoc-gen-validate]. +`protovalidate-java` is the Java language implementation of [`protovalidate`](https://github.com/bufbuild/protovalidate) designed to validate Protobuf messages at runtime based on user-defined validation constraints. Powered by Google's Common Expression Language ([CEL](https://github.com/google/cel-spec)), it provides a flexible and efficient foundation for defining and evaluating custom validation rules. The primary goal of `protovalidate` is to help developers ensure data consistency and integrity across the network without requiring generated code. -With Protovalidate, you can annotate your Protobuf messages with both standard and custom validation rules: +## The `protovalidate` project -```protobuf -syntax = "proto3"; +Head over to the core [`protovalidate`](https://github.com/bufbuild/protovalidate/) repository for: -package acme.user.v1; - -import "buf/validate/validate.proto"; +- [The API definition](https://github.com/bufbuild/protovalidate/tree/main/proto/protovalidate/buf/validate/validate.proto): used to describe validation constraints +- [Documentation](https://github.com/bufbuild/protovalidate/tree/main/docs): how to apply `protovalidate` effectively +- [Migration tooling](https://github.com/bufbuild/protovalidate/tree/main/docs/migrate.md): incrementally migrate from `protoc-gen-validate` +- [Conformance testing utilities](https://github.com/bufbuild/protovalidate/tree/main/docs/conformance.md): for acceptance testing of `protovalidate` implementations -message User { - string id = 1 [(buf.validate.field).string.uuid = true]; - uint32 age = 2 [(buf.validate.field).uint32.lte = 150]; // We can only hope. - string email = 3 [(buf.validate.field).string.email = true]; - string first_name = 4 [(buf.validate.field).string.max_len = 64]; - string last_name = 5 [(buf.validate.field).string.max_len = 64]; +Other `protovalidate` runtime implementations include: - option (buf.validate.message).cel = { - id: "first_name_requires_last_name" - message: "last_name must be present if first_name is present" - expression: "!has(this.first_name) || has(this.last_name)" - }; -} -``` +- C++: [`protovalidate-cc`](https://github.com/bufbuild/protovalidate-cc) +- Go: [`protovalidate-go`](https://github.com/bufbuild/protovalidate-go) +- Python: [`protovalidate-python`](https://github.com/bufbuild/protovalidate-python) -Once you've added `protovalidate-java` to your project, validation is idiomatic Java: +And others coming soon: -```java -ValidationResult result = validator.validate(message); -if (!result.isSuccess()) { - // Handle failure. -} -``` +- TypeScript: `protovalidate-ts` ## Installation -> [!TIP] -> The easiest way to get started with Protovalidate for RPC APIs are the quickstarts in Buf's documentation. There's one available for [Java and gRPC][grpc-java]. - -`protovalidate-java` is listed in [Maven Central][maven], which provides installation snippets for Gradle, Maven, and other package managers. In Gradle, it's: +To include `protovalidate-java` in your project, add the following to your build file: ```gradle dependencies { @@ -54,82 +35,109 @@ dependencies { } ``` -## Documentation +Remember to always check for the latest version of `protovalidate-java` on the project's [GitHub releases page](https://github.com/bufbuild/protovalidate-java/releases) to ensure you're using the most up-to-date version. -Comprehensive documentation for Protovalidate is available at [protovalidate.com][protovalidate]. +## Usage -Highlights for Java developers include: +### Implementing validation constraints -* The [developer quickstart][quickstart] -* A comprehensive RPC quickstart for [Java and gRPC][grpc-java] -* A [migration guide for protoc-gen-validate][migration-guide] users +Validation constraints are defined directly within `.proto` files. Documentation for adding constraints can be found in the `protovalidate` project [README](https://github.com/bufbuild/protovalidate) and its [comprehensive docs](https://github.com/bufbuild/protovalidate/tree/main/docs). -## Native rule evaluators (opt-out) +```protobuf +syntax = "proto3"; -The standard rules can be evaluated either through CEL or through native Java code. Native evaluation is functionally identical (the conformance suite passes in both modes) but skips CEL compilation and runtime overhead for the rules it covers — a single `validate()` call on a complex message can run an order of magnitude faster and allocate ~10× less. +package my.package; -Native rules are **opt-out**. Disable them by configuring the validator: +import "google/protobuf/timestamp.proto"; +import "buf/validate/validate.proto"; -```java -Config config = Config.newBuilder().setEnableNativeRules(false).build(); -Validator validator = ValidatorFactory.newBuilder().withConfig(config).build(); +message Transaction { + uint64 id = 1 [(buf.validate.field).uint64.gt = 999]; + google.protobuf.Timestamp purchase_date = 2; + google.protobuf.Timestamp delivery_date = 3; + + string price = 4 [(buf.validate.field).cel = { + id: "transaction.price", + message: "price must be positive and include a valid currency symbol ($ or £)", + expression: "(this.startsWith('$') || this.startsWith('£')) && double(this.substring(1)) > 0" + }]; + + option (buf.validate.message).cel = { + id: "transaction.delivery_date", + message: "delivery date must be after purchase date", + expression: "this.delivery_date > this.purchase_date" + }; +} ``` -Forward compatibility is preserved by a clone-and-clear contract: when protovalidate adds a new rule that this codebase hasn't yet implemented natively, the rule remains on the residual `FieldRules` and CEL enforces it. Native evaluation is an optimization, never a replacement. - -## Additional languages and repositories - -Protovalidate isn't just for Java! You might be interested in sibling repositories for other languages: - -- [`protovalidate-go`][pv-go] (Go) -- [`protovalidate-python`][pv-python] (Python) -- [`protovalidate-cc`][pv-cc] (C++) -- [`protovalidate-es`][pv-es] (TypeScript and JavaScript) +### Example -Additionally, [protovalidate's core repository](https://github.com/bufbuild/protovalidate) provides: +In your Java code, create an instance of the `Validator` class and use the `validate` method to validate your messages. -- [Protovalidate's Protobuf API][validate-proto] -- [Conformance testing utilities][conformance] for acceptance testing of `protovalidate` implementations - - -## Contributing +```java +// Import the required packages +package build.buf; + +import build.buf.protovalidate.results.ValidationException; +import build.buf.protovalidate.results.ValidationResult; +import com.my.package.Transaction; +import com.google.protobuf.Timestamp; + +import build.buf.protovalidate.Validator; +import build.buf.protovalidate.Config; + +public class Main { + + // Create timestamps for purchase and delivery date + Timestamp purchaseDate = Timestamp.newBuilder().build(); + Timestamp deliveryDate = Timestamp.newBuilder().build(); + + // Create a transaction object using the Builder pattern + Transaction transaction = + Transaction.newBuilder() + .setId(1234) + .setPrice("$5.67") + .setPurchaseDate(purchaseDate) + .setDeliveryDate(deliveryDate) + .build(); + + // Create a Config instance + Config config = Config.builder().build(); + // Create a validator object with your Config instance + Validator validator = new Validator(config); + + { + // Validate the transaction object using the validator + try { + ValidationResult result = validator.validate(transaction); + + // Check if there are any validation violations + if (result.violations.isEmpty()) { + // No violations, validation successful + System.out.println("Validation succeeded"); + } else { + // Print the violations if any found + System.out.println(result.getMessage()); + } + } catch (ValidationException e) { + // Catch and print any ValidationExceptions thrown during the validation process + System.out.println("Validation failed: " + e.getMessage()); + } + } +} +``` -We genuinely appreciate any help! If you'd like to contribute, check out these resources: +### Ecosystem -- [Contributing Guidelines][contributing]: Guidelines to make your contribution process straightforward and meaningful -- [Conformance testing utilities](https://github.com/bufbuild/protovalidate/tree/main/docs/conformance.md): Utilities providing acceptance testing of `protovalidate` implementations +- [`protovalidate`](https://github.com/bufbuild/protovalidate) core repository +- [Buf][buf] +- [CEL Spec][cel-spec] ## Legal Offered under the [Apache 2 license][license]. -[buf]: https://buf.build -[cel]: https://cel.dev - -[pv-go]: https://github.com/bufbuild/protovalidate-go -[pv-java]: https://github.com/bufbuild/protovalidate-java -[pv-python]: https://github.com/bufbuild/protovalidate-python -[pv-cc]: https://github.com/bufbuild/protovalidate-cc -[pv-es]: https://github.com/bufbuild/protovalidate-es - [license]: LICENSE -[contributing]: .github/CONTRIBUTING.md +[buf]: https://buf.build [buf-mod]: https://buf.build/bufbuild/protovalidate - -[protoc-gen-validate]: https://github.com/bufbuild/protoc-gen-validate - -[protovalidate]: https://protovalidate.com -[quickstart]: https://protovalidate.com/quickstart/ -[connect-go]: https://protovalidate.com/quickstart/connect-go/ -[grpc-go]: https://protovalidate.com/quickstart/grpc-go/ -[grpc-java]: https://protovalidate.com/quickstart/grpc-java/ -[grpc-python]: https://protovalidate.com/quickstart/grpc-python/ -[migration-guide]: https://protovalidate.com/migration-guides/migrate-from-protoc-gen-validate/ - -[maven]: https://central.sonatype.com/artifact/build.buf/protovalidate/overview -[pkg-go]: https://pkg.go.dev/github.com/bufbuild/protovalidate-go - -[validate-proto]: https://buf.build/bufbuild/protovalidate/docs/main:buf.validate -[conformance]: https://github.com/bufbuild/protovalidate/blob/main/docs/conformance.md -[examples]: https://github.com/bufbuild/protovalidate/tree/main/examples -[migrate]: https://protovalidate.com/migration-guides/migrate-from-protoc-gen-validate/ +[cel-spec]: https://github.com/google/cel-spec diff --git a/conformance/expected-failures.yaml b/asdf similarity index 100% rename from conformance/expected-failures.yaml rename to asdf diff --git a/benchmarks/README.md b/benchmarks/README.md deleted file mode 100644 index fe12957e5..000000000 --- a/benchmarks/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Benchmarks - -JMH microbenchmarks for protovalidate-java. -Used locally to quantify performance changes. -Not executed in CI; `./gradlew build` only verifies that benchmark code compiles. - -## Prerequisites - -- JDK 21 -- `buf` CLI (installed automatically by Gradle) -- `jq` and `column` (preinstalled on macOS) - -## Running benchmarks - -Run all benchmarks: - -``` -./gradlew :benchmarks:jmh -``` - -Filter to a subset via `-Pbench` (accepts a regex over method names): - -``` -./gradlew :benchmarks:jmh -Pbench=validateSimple # one method -./gradlew :benchmarks:jmh -Pbench='compile.*' # prefix match -./gradlew :benchmarks:jmh -Pbench='validate.*' # all steady-state -``` - -Results land in `build/results/jmh/results.json`. - -## Comparing before and after a change - -Typical A/B workflow: - -``` -# 1. run baseline on the current tree and save it -./gradlew :benchmarks:jmh -Pbench='compile.*' :benchmarks:jmhSaveBaseline - -# 2. apply your change (edit code, or gh pr checkout ) - -# 3. re-run and diff against the saved baseline -./gradlew :benchmarks:jmh -Pbench='compile.*' :benchmarks:jmhCompare -``` - -Output: - -``` -benchmark metric before after delta -compileValidatorForRepeated time 4696209.43 ns/op 1064942.21 ns/op -77.3% -compileValidatorForRepeated alloc 12950196.95 B/op 3262651.61 B/op -74.8% -``` - -`jmhSaveBaseline` copies the current `results.json` to `results-before.json`. -`jmhCompare` diffs `results-before.json` against `results.json` by default. -Pass explicit paths with `-Pbefore= -Pafter=`. - -## Comparing native rules vs CEL - -Benchmarks A/B the `enableNativeRules` flag via `@Param({"false", "true"})`, so a single run produces both variants. -Diff them in place: - -``` -./gradlew :benchmarks:jmh -./gradlew :benchmarks:jmhCompareNativeRules -``` - -Output (`before` = CEL, `after` = native; negative delta means native is faster / allocates less): - -``` -benchmark metric cel native delta -buildBenchInt32GT time 1234567.89 ns/op 456789.01 ns/op -63.0% -buildBenchInt32GT alloc 123456.78 B/op 45678.90 B/op -63.0% -``` - -Override the input file with `-Presults=`. - -## Adding a new benchmark - -Benchmarks live in `src/jmh/java/...` and target proto messages in `src/jmh/proto/...`. - -### 1. Define (or reuse) a proto message - -Edit `src/jmh/proto/bench/v1/bench.proto` to add a message that exercises the code path you want to measure. -`buf generate` runs automatically before `compileJmhJava`, so no separate codegen step is needed. - -### 2. Add a `@Benchmark` method - -Edit `src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java`. -Put one-time state (validator, messages) in `@Setup` and the measured work in the `@Benchmark` method. - -Steady-state (hot-path) pattern: - -```java -@Benchmark -public void validateMyMessage(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(myMessage)); -} -``` - -Cold/compile-path pattern (each iteration builds a fresh validator): - -```java -@Benchmark -@OutputTimeUnit(TimeUnit.MILLISECONDS) -public void compileValidatorForMyMessage(Blackhole bh) throws CompilationException { - Validator v = ValidatorFactory.newBuilder() - .buildWithDescriptors(Collections.singletonList(MyMessage.getDescriptor()), false); - bh.consume(v); -} -``` - -Choose based on what the change you want to measure actually touches. -`EvaluatorBuilder` caches compiled evaluators per descriptor, so after the first `validate()` call, further calls skip compilation. -If your fix is in the compile path (e.g. `RuleCache`, `DescriptorCacheBuilder`), a steady-state benchmark will not show the effect because `@Setup` absorbs it. - -## Configuration - -`build.gradle.kts` holds the JMH plugin config. -Defaults are tuned for fast local iteration (~30s per benchmark): - -- 3 warmup iterations of 2s each -- 5 measurement iterations of 2s each -- 2 forks -- Average-time mode, nanoseconds -- GC profiler on (`gc.alloc.rate.norm` for per-op allocations) - -For higher-confidence numbers (tighter confidence intervals, useful for deltas under ~10%), bump `fork`, `warmup`, and `timeOnIteration` in the `jmh {}` block. -Expect ~5 min per benchmark at `fork=5, warmup=5s, timeOnIteration=5s`. - -## Metrics - -Each benchmark emits: - -- **Primary:** average time per `@Benchmark` invocation (`ns/op` by default). -- **Secondary (GC profiler):** - - `gc.alloc.rate.norm` - bytes allocated per op; deterministic, used by `jmhCompare`. - - `gc.alloc.rate` - allocation rate in MB/sec; varies with CPU. - - `gc.count` / `gc.time` - GC activity during the run. - -For allocation flame graphs, uncomment the `async` profiler line in `build.gradle.kts`. -Requires `async-profiler` installed locally. diff --git a/benchmarks/buf.gen.yaml b/benchmarks/buf.gen.yaml deleted file mode 100644 index 4434781f5..000000000 --- a/benchmarks/buf.gen.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: v2 -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/sources/bufgen -inputs: - - directory: src/jmh/proto diff --git a/benchmarks/buf.lock b/benchmarks/buf.lock deleted file mode 100644 index 709ae0239..000000000 --- a/benchmarks/buf.lock +++ /dev/null @@ -1,6 +0,0 @@ -# Generated by buf. DO NOT EDIT. -version: v2 -deps: - - name: buf.build/bufbuild/protovalidate - commit: 50325440f8f24053b047484a6bf60b76 - digest: b5:74cb6f5c0853c3c10aafc701614194bbd63326bdb8ef4068214454b8894b03ba4113e04b3a33a8321cdf05336e37db4dc14a5e2495db8462566914f36086ba31 diff --git a/benchmarks/buf.yaml b/benchmarks/buf.yaml deleted file mode 100644 index 56bbef242..000000000 --- a/benchmarks/buf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: v2 -modules: - - path: src/jmh/proto -deps: - - buf.build/bufbuild/protovalidate diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts deleted file mode 100644 index 8186a0d76..000000000 --- a/benchmarks/build.gradle.kts +++ /dev/null @@ -1,168 +0,0 @@ -import com.diffplug.gradle.spotless.SpotlessExtension - -plugins { - java - alias(libs.plugins.jmh) - alias(libs.plugins.osdetector) -} - -// JMH can use modern bytecode; benchmarks aren't shipped. -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -configure { - java { - targetExclude("build/generated/**/*.java") - } -} - -val buf: Configuration by configurations.creating - -tasks.register("configureBuf") { - description = "Installs the Buf CLI." - File(buf.asPath).setExecutable(true) -} - -tasks.register("filterBufGenYaml") { - from(files("buf.gen.yaml")) - includeEmptyDirs = false - into(layout.buildDirectory.dir("buf-gen-templates")) - expand("protocJavaPluginVersion" to "v${libs.versions.protobuf.get().substringAfter('.')}") - filteringCharset = "UTF-8" -} - -tasks.register("generateBenchmarkSources") { - dependsOn("configureBuf", "filterBufGenYaml") - description = "Generates Java sources for benchmark protos via buf generate." - val template = layout.buildDirectory.file("buf-gen-templates/buf.gen.yaml") - inputs.files(buf) - inputs.dir("src/jmh/proto") - inputs.file("buf.yaml") - inputs.file(template) - outputs.dir(layout.buildDirectory.dir("generated/sources/bufgen")) - commandLine(buf.asPath, "generate", "--template", template.get().asFile.absolutePath) -} - -sourceSets { - named("jmh") { - java { - srcDir(layout.buildDirectory.dir("generated/sources/bufgen")) - } - } -} - -tasks.matching { it.name == "compileJmhJava" }.configureEach { - dependsOn("generateBenchmarkSources") -} - -// Ensure `./gradlew build` (and `make build`) compiles the JMH sources so CI -// catches breakages in benchmark code. Execution remains gated behind the -// explicit `:benchmarks:jmh` task. -tasks.named("build") { - dependsOn("compileJmhJava") -} - -dependencies { - jmhImplementation(project(":")) - jmhImplementation(libs.protobuf.java) - buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe") -} - -// Benchmarks produce fresh timing data each run; disable Gradle's up-to-date -// check so the task always executes (otherwise -Pbench changes are ignored). -tasks.named("jmh") { - outputs.upToDateWhen { false } -} - -jmh { - // Defaults tuned for fast local A/B runs (~90s total). - // For higher-confidence numbers bump iteration time and fork count. - warmupIterations.set(3) - warmup.set("2s") - iterations.set(5) - timeOnIteration.set("2s") - fork.set(2) - timeUnit.set("ns") - benchmarkMode.set(listOf("avgt")) - resultFormat.set("JSON") - // GC profiler reports bytes allocated per op (gc.alloc.rate.norm), which - // jmhCompare can diff alongside timing. ~5-10% overhead on timings. - profilers.set(listOf("gc")) - // For allocation flame graphs (requires async-profiler installed locally): - // profilers.set(listOf("async:event=alloc;output=flamegraph;dir=build/reports/jmh/async")) - - // Filter to a subset of benchmarks via `-Pbench=`. Example: - // ./gradlew :benchmarks:jmh -Pbench=validateSimple - // ./gradlew :benchmarks:jmh -Pbench='compile.*' - project.findProperty("bench")?.toString()?.let { - includes.set(listOf(it)) - } -} - -val jmhResults = layout.buildDirectory.file("results/jmh/results.json") -val jmhBaseline = layout.buildDirectory.file("results/jmh/results-before.json") - -// Saves the latest JMH results.json as the baseline for jmhCompare. -// -// Usage: -// ./gradlew :benchmarks:jmh :benchmarks:jmhSaveBaseline -// # apply change... -// ./gradlew :benchmarks:jmh :benchmarks:jmhCompare -tasks.register("jmhSaveBaseline") { - description = "Copies the latest JMH results.json to results-before.json as the baseline." - from(jmhResults) - into(jmhResults.get().asFile.parentFile) - rename { "results-before.json" } - mustRunAfter("jmh") -} - -// Diffs two JMH results.json files as a concise benchstat-style table. -// Defaults to comparing results-before.json (written by jmhSaveBaseline) -// against the latest results.json. -// -// Override paths: -// ./gradlew :benchmarks:jmhCompare -Pbefore=a.json -Pafter=b.json -tasks.register("jmhCompare") { - description = "Diffs two JMH result JSON files as a concise table." - val before = - project.findProperty("before")?.toString() - ?: jmhBaseline.get().asFile.absolutePath - val after = - project.findProperty("after")?.toString() - ?: jmhResults.get().asFile.absolutePath - val jqScript = file("jmh-compare.jq").absolutePath - commandLine( - "bash", - "-c", - "jq --slurp --raw-output --from-file \"\$1\" \"\$2\" \"\$3\" | column -t -s \$'\\t'", - "jmh-compare", // $0 - jqScript, // $1 - before, // $2 - after, // $3 - ) -} - -// Diffs the two enableNativeRules variants within a single results.json. -// `before` column is CEL (enableNativeRules=false), `after` is native -// (enableNativeRules=true), so a negative delta means native is faster / -// allocates less. -// -// Override the input file: -// ./gradlew :benchmarks:jmhCompareNative -Presults=path/to/results.json -tasks.register("jmhCompareNativeRules") { - description = "Diffs enableNativeRules=true vs false from a single JMH results.json." - val results = - project.findProperty("results")?.toString() - ?: jmhResults.get().asFile.absolutePath - val jqScript = file("jmh-compare-native-rules.jq").absolutePath - commandLine( - "bash", - "-c", - "jq --raw-output --from-file \"\$1\" \"\$2\" | column -t -s \$'\\t'", - "jmh-compare-native-rules", // $0 - jqScript, // $1 - results, // $2 - ) -} diff --git a/benchmarks/jmh-compare-native-rules.jq b/benchmarks/jmh-compare-native-rules.jq deleted file mode 100644 index fcec2004f..000000000 --- a/benchmarks/jmh-compare-native-rules.jq +++ /dev/null @@ -1,32 +0,0 @@ -# this script builds a comparison between runs that used the CEL interpreter for -# protovalidate rule evaluation vs runs that used native Java code for protovalidate -# rule evaluation. The differentiator is the value of the "native" field (true for -# native, false for CEL) and this script groups rows that have the same benchmark -# and metric name, but different values for native. -def pct(a; b): - if a == null or b == null or b == 0 then "~" - else (((a - b) / b * 100) * 10 | round / 10) as $d - | if $d > 0 then "+\($d)%" elif $d == 0 then "~" else "\($d)%" end - end; -def num(x): - if x == null then "-" - else (x * 100 | round / 100 | tostring) - end; - -def row: { - key: (.benchmark | split(".") | last), - native: .params.enableNativeRules, - time: .primaryMetric.score, - unit: .primaryMetric.scoreUnit, - alloc: (.secondaryMetrics["·gc.alloc.rate.norm"].score // null) -}; - -map(row) -| group_by(.key) -| (["benchmark", "metric", "cel", "native", "delta"] | @tsv), - (.[] - | (map(select(.native == "false"))[0]) as $b - | (map(select(.native == "true"))[0]) as $a - | select($b and $a) - | ([$b.key, "time", "\(num($b.time)) \($b.unit)", "\(num($a.time)) \($a.unit)", pct($a.time; $b.time)] | @tsv), - ([$b.key, "alloc", "\(num($b.alloc)) B/op", "\(num($a.alloc)) B/op", pct($a.alloc; $b.alloc)] | @tsv)) \ No newline at end of file diff --git a/benchmarks/jmh-compare.jq b/benchmarks/jmh-compare.jq deleted file mode 100644 index 4d015a362..000000000 --- a/benchmarks/jmh-compare.jq +++ /dev/null @@ -1,24 +0,0 @@ -def pct(a; b): - if a == null or b == null or b == 0 then "~" - else (((a - b) / b * 100) * 10 | round / 10) as $d - | if $d > 0 then "+\($d)%" elif $d == 0 then "~" else "\($d)%" end - end; -def num(x): - if x == null then "-" - else (x * 100 | round / 100 | tostring) - end; - -def extract: map({ - key: (.benchmark | split(".") | last), - time: .primaryMetric.score, - time_unit: .primaryMetric.scoreUnit, - alloc: (.secondaryMetrics["·gc.alloc.rate.norm"].score // null) -}); - -(.[0] | extract) as $b -| (.[1] | extract) as $a -| (["benchmark", "metric", "before", "after", "delta"] | @tsv), - ($b[] | . as $bi - | ($a[] | select(.key == $bi.key)) as $ai - | ([$bi.key, "time", "\(num($bi.time)) \($bi.time_unit)", "\(num($ai.time)) \($ai.time_unit)", pct($ai.time; $bi.time)] | @tsv), - ([$bi.key, "alloc", "\(num($bi.alloc)) B/op", "\(num($ai.alloc)) B/op", pct($ai.alloc; $bi.alloc)] | @tsv)) diff --git a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/BenchFixtures.java b/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/BenchFixtures.java deleted file mode 100644 index 95dd914f6..000000000 --- a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/BenchFixtures.java +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate.benchmarks; - -import build.buf.protovalidate.benchmarks.gen.BenchBoolConst; -import build.buf.protovalidate.benchmarks.gen.BenchBytesConst; -import build.buf.protovalidate.benchmarks.gen.BenchBytesIn; -import build.buf.protovalidate.benchmarks.gen.BenchComplexSchema; -import build.buf.protovalidate.benchmarks.gen.BenchDoubleIn; -import build.buf.protovalidate.benchmarks.gen.BenchEnum; -import build.buf.protovalidate.benchmarks.gen.BenchEnumConst; -import build.buf.protovalidate.benchmarks.gen.BenchEnumNotIn; -import build.buf.protovalidate.benchmarks.gen.BenchEnumRules; -import build.buf.protovalidate.benchmarks.gen.BenchGT; -import build.buf.protovalidate.benchmarks.gen.BenchInt64Const; -import build.buf.protovalidate.benchmarks.gen.BenchInt64In; -import build.buf.protovalidate.benchmarks.gen.BenchMap; -import build.buf.protovalidate.benchmarks.gen.BenchPhaseEnum; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedBytesUnique; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedInt32Unique; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedMessage; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedScalar; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedScalarUnique; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedStringUnique; -import build.buf.protovalidate.benchmarks.gen.BenchScalar; -import build.buf.protovalidate.benchmarks.gen.BenchStringConst; -import build.buf.protovalidate.benchmarks.gen.BenchStringContains; -import build.buf.protovalidate.benchmarks.gen.BenchStringIn; -import build.buf.protovalidate.benchmarks.gen.BenchStringLen; -import build.buf.protovalidate.benchmarks.gen.BenchStringMinLen; -import build.buf.protovalidate.benchmarks.gen.BenchStringPrefix; -import build.buf.protovalidate.benchmarks.gen.BenchUint32In; -import build.buf.protovalidate.benchmarks.gen.MultiRule; -import build.buf.protovalidate.benchmarks.gen.StringMatching; -import build.buf.protovalidate.benchmarks.gen.TestByteMatching; -import build.buf.protovalidate.benchmarks.gen.WrapperTesting; -import com.google.protobuf.BoolValue; -import com.google.protobuf.ByteString; -import com.google.protobuf.BytesValue; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.FloatValue; -import com.google.protobuf.Int32Value; -import com.google.protobuf.Int64Value; -import com.google.protobuf.StringValue; -import com.google.protobuf.UInt32Value; -import com.google.protobuf.UInt64Value; - -/** - * Hand-built deterministic fixtures for the native-rules benchmark suite. - * - *

Each factory returns a fully-populated message that satisfies all of its validation rules. - * Values are literal (no Random) so benchmarks are reproducible run-to-run. Chosen to match the - * intent of the gofakeit annotations in the Go reference protos without depending on a faker - * library. - */ -final class BenchFixtures { - private BenchFixtures() {} - - static BenchScalar benchScalar() { - return BenchScalar.newBuilder().setX(42).build(); - } - - static BenchRepeatedScalar benchRepeatedScalar() { - BenchRepeatedScalar.Builder b = BenchRepeatedScalar.newBuilder(); - for (int i = 1; i <= 5; i++) { - b.addX(i); - } - return b.build(); - } - - static BenchRepeatedMessage benchRepeatedMessage() { - BenchRepeatedMessage.Builder b = BenchRepeatedMessage.newBuilder(); - for (int i = 1; i <= 5; i++) { - b.addX(BenchScalar.newBuilder().setX(i).build()); - } - return b.build(); - } - - static BenchRepeatedScalarUnique benchRepeatedScalarUnique() { - BenchRepeatedScalarUnique.Builder b = BenchRepeatedScalarUnique.newBuilder(); - for (int i = 1; i <= 8; i++) { - b.addX((float) i); - } - return b.build(); - } - - static BenchRepeatedBytesUnique benchRepeatedBytesUnique() { - BenchRepeatedBytesUnique.Builder b = BenchRepeatedBytesUnique.newBuilder(); - for (int i = 1; i <= 8; i++) { - b.addX(ByteString.copyFromUtf8("entry-" + i)); - } - return b.build(); - } - - static BenchMap benchMap() { - BenchMap.Builder b = BenchMap.newBuilder(); - for (int i = 1; i <= 5; i++) { - b.putEntries("key-" + i, "value-" + i); - } - return b.build(); - } - - static BenchComplexSchema benchComplexSchema() { - BenchComplexSchema.Builder b = - BenchComplexSchema.newBuilder() - .setS1("hello") - .setS2("world") - .setI32(42) - .setI64(42L) - .setU32(42) - .setU64(42L) - .setSi32(42) - .setSi64(42L) - .setF32(42) - .setF64(42L) - .setSf32(42) - .setSf64(42L) - .setFl(42.0f) - .setDb(42.0) - .setBl(true) - .setBy(ByteString.copyFromUtf8("payload")) - .setNested(BenchScalar.newBuilder().setX(1).build()) - // self_ref intentionally left null; proto3 message fields default to absent - .setEnumField(BenchEnum.BENCH_ENUM_ONE) - .setOneofStr("hello"); - - for (int i = 1; i <= 3; i++) { - b.addRepStr("item-" + i); - b.addRepI32(i); - b.addRepBytes(ByteString.copyFromUtf8("bytes-" + i)); - b.addRepMsg(BenchScalar.newBuilder().setX(i).build()); - } - - for (int i = 1; i <= 3; i++) { - b.putMapStrStr("k" + i, "v" + i); - b.putMapI32I64(i, (long) i); - b.putMapU64Bool((long) i, i % 2 == 0); - b.putMapStrBytes("k" + i, ByteString.copyFromUtf8("v" + i)); - b.putMapStrMsg("k" + i, BenchScalar.newBuilder().setX(i).build()); - b.putMapI64Msg((long) i, BenchScalar.newBuilder().setX(i).build()); - } - - return b.build(); - } - - static BenchGT benchGT() { - // For gt > lt / gte > lte cases, protovalidate interprets the range as - // exclusive (value not in [lt, gt]). 50 is outside [-20, 0] for all four. - return BenchGT.newBuilder() - .setGt(50) - .setGte(50) - .setLt(50) - .setLte(50) - .setGtltin(50) - .setGtltein(50) - .setGtltex(50) - .setGtlteex(50) - .setGteltin(50) - .setGteltein(50) - .setGteltex(50) - .setGtelteex(50) - .setConst(10) - .setConstgt(10) - .setInTest(3) - .setNotInTest(4) - .build(); - } - - static TestByteMatching testByteMatching() { - return TestByteMatching.newBuilder() - .setIpAddr(ByteString.copyFrom(new byte[16])) // any 16 bytes (ip rule = 4 or 16) - .setIpv4Addr(ByteString.copyFrom(new byte[4])) - .setIpv6Addr(ByteString.copyFrom(new byte[16])) - .setUuid(ByteString.copyFrom(new byte[16])) - .build(); - } - - static StringMatching stringMatching() { - return StringMatching.newBuilder() - .setHostname("example.com") - .setHostAndPort("example.com:8080") - .setEmail("alice@example.com") - .setUuid("550e8400-e29b-41d4-a716-446655440000") - .build(); - } - - static WrapperTesting wrapperTesting() { - return WrapperTesting.newBuilder() - .setI32(Int32Value.of(11)) - .setD(DoubleValue.of(11)) - .setF(FloatValue.of(11)) - .setI64(Int64Value.of(11)) - .setU64(UInt64Value.of(11)) - .setU32(UInt32Value.of(11)) - .setB(BoolValue.of(true)) - .setS(StringValue.of("hello")) - .setBs(BytesValue.of(ByteString.copyFromUtf8("hello"))) - .build(); - } - - /** Multi-rule fixture that PASSES — many=10 satisfies const=10 and gt=5. */ - static MultiRule multiRuleNoError() { - return MultiRule.newBuilder().setMany(10).build(); - } - - /** Multi-rule fixture that FAILS both rules — many=1 violates const=10 and gt=5. */ - static MultiRule multiRuleError() { - return MultiRule.newBuilder().setMany(1).build(); - } - - /** Phase 2 measurement target — exercises BoolRulesEvaluator on bool.const. */ - static BenchBoolConst benchBoolConst() { - return BenchBoolConst.newBuilder().setFlag(true).build(); - } - - /** Phase 4 measurement target — exercises EnumRulesEvaluator on enum.in. */ - static BenchEnumRules benchEnumRules() { - return BenchEnumRules.newBuilder().setVal(BenchPhaseEnum.BENCH_PHASE_ENUM_TWO).build(); - } - - // --- Single-rule fixtures for previously unbenchmarked rules --- - // Each fixture's value satisfies its rule — benchmarks measure the happy path. - - static BenchStringConst benchStringConst() { - return BenchStringConst.newBuilder().setS("hello").build(); - } - - static BenchStringLen benchStringLen() { - return BenchStringLen.newBuilder().setS("hello").build(); - } - - static BenchStringMinLen benchStringMinLen() { - return BenchStringMinLen.newBuilder().setS("x").build(); - } - - static BenchStringPrefix benchStringPrefix() { - return BenchStringPrefix.newBuilder().setS("user-alice").build(); - } - - static BenchStringContains benchStringContains() { - return BenchStringContains.newBuilder().setS("alice@example.com").build(); - } - - static BenchStringIn benchStringIn() { - return BenchStringIn.newBuilder().setS("bar").build(); - } - - static BenchBytesConst benchBytesConst() { - return BenchBytesConst.newBuilder().setB(ByteString.copyFromUtf8("abc")).build(); - } - - static BenchBytesIn benchBytesIn() { - return BenchBytesIn.newBuilder().setB(ByteString.copyFromUtf8("bar")).build(); - } - - static BenchInt64Const benchInt64Const() { - return BenchInt64Const.newBuilder().setV(42L).build(); - } - - static BenchInt64In benchInt64In() { - return BenchInt64In.newBuilder().setV(2L).build(); - } - - static BenchUint32In benchUint32In() { - return BenchUint32In.newBuilder().setV(2).build(); - } - - static BenchDoubleIn benchDoubleIn() { - return BenchDoubleIn.newBuilder().setV(2.0).build(); - } - - static BenchEnumConst benchEnumConst() { - return BenchEnumConst.newBuilder().setVal(BenchPhaseEnum.BENCH_PHASE_ENUM_ONE).build(); - } - - static BenchEnumNotIn benchEnumNotIn() { - return BenchEnumNotIn.newBuilder().setVal(BenchPhaseEnum.BENCH_PHASE_ENUM_ONE).build(); - } - - static BenchRepeatedStringUnique benchRepeatedStringUnique() { - BenchRepeatedStringUnique.Builder b = BenchRepeatedStringUnique.newBuilder(); - for (int i = 1; i <= 8; i++) { - b.addX("entry-" + i); - } - return b.build(); - } - - static BenchRepeatedInt32Unique benchRepeatedInt32Unique() { - BenchRepeatedInt32Unique.Builder b = BenchRepeatedInt32Unique.newBuilder(); - for (int i = 1; i <= 8; i++) { - b.addX(i); - } - return b.build(); - } -} diff --git a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/EvaluatorBuildBenchmark.java b/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/EvaluatorBuildBenchmark.java deleted file mode 100644 index 8f22a121a..000000000 --- a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/EvaluatorBuildBenchmark.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate.benchmarks; - -import build.buf.protovalidate.Config; -import build.buf.protovalidate.Validator; -import build.buf.protovalidate.ValidatorFactory; -import build.buf.protovalidate.benchmarks.gen.BenchComplexSchema; -import build.buf.protovalidate.benchmarks.gen.BenchGT; -import build.buf.protovalidate.exceptions.ValidationException; -import com.google.protobuf.Message; -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.infra.Blackhole; - -/** - * Compile-time evaluator construction benchmarks. Mirrors Go's {@code BenchmarkCompile} and {@code - * BenchmarkCompileInt32GT}. These measure how long it takes to build a validator (compile rules, - * cache evaluators) for a given message type — the cost paid once per descriptor. - */ -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -public class EvaluatorBuildBenchmark { - - @Param({"false", "true"}) - public boolean enableNativeRules; - - private Config config; - private Message benchComplexSchema; - private Message benchGT; - - @Setup - public void setup() { - config = Config.newBuilder().setEnableNativeRules(enableNativeRules).build(); - benchComplexSchema = BenchComplexSchema.getDefaultInstance(); - benchGT = BenchGT.getDefaultInstance(); - } - - @Benchmark - public Validator buildBenchComplexSchema(Blackhole bh) throws ValidationException { - Validator v = ValidatorFactory.newBuilder().withConfig(config).build(); - // Force evaluator construction by validating the default instance. - bh.consume(v.validate(benchComplexSchema)); - return v; - } - - @Benchmark - public Validator buildBenchInt32GT(Blackhole bh) throws ValidationException { - Validator v = ValidatorFactory.newBuilder().withConfig(config).build(); - bh.consume(v.validate(benchGT)); - return v; - } -} diff --git a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java b/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java deleted file mode 100644 index 93d34bc9b..000000000 --- a/benchmarks/src/jmh/java/build/buf/protovalidate/benchmarks/ValidationBenchmark.java +++ /dev/null @@ -1,411 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate.benchmarks; - -import build.buf.protovalidate.Config; -import build.buf.protovalidate.Validator; -import build.buf.protovalidate.ValidatorFactory; -import build.buf.protovalidate.benchmarks.gen.BenchBoolConst; -import build.buf.protovalidate.benchmarks.gen.BenchBytesConst; -import build.buf.protovalidate.benchmarks.gen.BenchBytesIn; -import build.buf.protovalidate.benchmarks.gen.BenchComplexSchema; -import build.buf.protovalidate.benchmarks.gen.BenchDoubleIn; -import build.buf.protovalidate.benchmarks.gen.BenchEnumConst; -import build.buf.protovalidate.benchmarks.gen.BenchEnumNotIn; -import build.buf.protovalidate.benchmarks.gen.BenchEnumRules; -import build.buf.protovalidate.benchmarks.gen.BenchGT; -import build.buf.protovalidate.benchmarks.gen.BenchInt64Const; -import build.buf.protovalidate.benchmarks.gen.BenchInt64In; -import build.buf.protovalidate.benchmarks.gen.BenchMap; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedBytesUnique; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedInt32Unique; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedMessage; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedScalar; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedScalarUnique; -import build.buf.protovalidate.benchmarks.gen.BenchRepeatedStringUnique; -import build.buf.protovalidate.benchmarks.gen.BenchScalar; -import build.buf.protovalidate.benchmarks.gen.BenchStringConst; -import build.buf.protovalidate.benchmarks.gen.BenchStringContains; -import build.buf.protovalidate.benchmarks.gen.BenchStringIn; -import build.buf.protovalidate.benchmarks.gen.BenchStringLen; -import build.buf.protovalidate.benchmarks.gen.BenchStringMinLen; -import build.buf.protovalidate.benchmarks.gen.BenchStringPrefix; -import build.buf.protovalidate.benchmarks.gen.BenchUint32In; -import build.buf.protovalidate.benchmarks.gen.ManyUnruledFieldsMessage; -import build.buf.protovalidate.benchmarks.gen.MultiRule; -import build.buf.protovalidate.benchmarks.gen.RegexPatternMessage; -import build.buf.protovalidate.benchmarks.gen.RepeatedRuleMessage; -import build.buf.protovalidate.benchmarks.gen.SimpleStringMessage; -import build.buf.protovalidate.benchmarks.gen.StringMatching; -import build.buf.protovalidate.benchmarks.gen.TestByteMatching; -import build.buf.protovalidate.benchmarks.gen.WrapperTesting; -import build.buf.protovalidate.exceptions.ValidationException; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Param; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.infra.Blackhole; - -/** - * Steady-state validation benchmarks. Exercises the hot path after the evaluator cache is warm. - * - *

The set of {@code validateBench*} methods mirrors the Go benchmark suite in protovalidate-go's - * {@code validator_bench_test.go} and provides the baseline against which the native-rules port - * measures its improvements. The original {@code validate*} methods exercise past PR fixes - * (tautology skip, AST cache, etc.) and remain as regression guards. - * - *

The {@code enableNativeRules} parameter A/Bs the native-rules flag: {@code "false"} matches - * the Phase 0 CEL-only baseline; {@code "true"} measures native evaluation. Each subsequent phase - * reports the gap between the two modes for its covered benchmarks. - */ -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -public class ValidationBenchmark { - - @Param({"false", "true"}) - public boolean enableNativeRules; - - private Validator validator; - - // --- Existing regression-guard fixtures --- - private SimpleStringMessage simple; - private ManyUnruledFieldsMessage manyUnruled; - private RepeatedRuleMessage repeatedRule; - private RegexPatternMessage regexPattern; - - // --- Native-rules port fixtures --- - private BenchScalar benchScalar; - private BenchRepeatedScalar benchRepeatedScalar; - private BenchRepeatedMessage benchRepeatedMessage; - private BenchRepeatedScalarUnique benchRepeatedScalarUnique; - private BenchRepeatedBytesUnique benchRepeatedBytesUnique; - private BenchMap benchMap; - private BenchComplexSchema benchComplexSchema; - private BenchGT benchGT; - private TestByteMatching testByteMatching; - private StringMatching stringMatching; - private WrapperTesting wrapperTesting; - private MultiRule multiRuleNoError; - private MultiRule multiRuleError; - private BenchBoolConst benchBoolConst; - private BenchEnumRules benchEnumRules; - - // Single-rule fixtures filling earlier coverage gaps. - private BenchStringConst benchStringConst; - private BenchStringLen benchStringLen; - private BenchStringMinLen benchStringMinLen; - private BenchStringPrefix benchStringPrefix; - private BenchStringContains benchStringContains; - private BenchStringIn benchStringIn; - private BenchBytesConst benchBytesConst; - private BenchBytesIn benchBytesIn; - private BenchInt64Const benchInt64Const; - private BenchInt64In benchInt64In; - private BenchUint32In benchUint32In; - private BenchDoubleIn benchDoubleIn; - private BenchEnumConst benchEnumConst; - private BenchEnumNotIn benchEnumNotIn; - private BenchRepeatedStringUnique benchRepeatedStringUnique; - private BenchRepeatedInt32Unique benchRepeatedInt32Unique; - - @Setup - public void setup() throws ValidationException { - Config config = Config.newBuilder().setEnableNativeRules(enableNativeRules).build(); - - validator = ValidatorFactory.newBuilder().withConfig(config).build(); - - simple = SimpleStringMessage.newBuilder().setEmail("alice@example.com").build(); - - manyUnruled = - ManyUnruledFieldsMessage.newBuilder() - .setNonEmpty("x") - .setF1("v1") - .setF2("v2") - .setF3("v3") - .setF4("v4") - .setF5("v5") - .setF6("v6") - .setF7("v7") - .setF8("v8") - .setF9("v9") - .build(); - - RepeatedRuleMessage.Builder repeatedRuleBuilder = RepeatedRuleMessage.newBuilder(); - for (FieldDescriptor fd : RepeatedRuleMessage.getDescriptor().getFields()) { - repeatedRuleBuilder.setField(fd, "v"); - } - repeatedRule = repeatedRuleBuilder.build(); - - regexPattern = RegexPatternMessage.newBuilder().setName("Alice Example").build(); - - benchScalar = BenchFixtures.benchScalar(); - benchRepeatedScalar = BenchFixtures.benchRepeatedScalar(); - benchRepeatedMessage = BenchFixtures.benchRepeatedMessage(); - benchRepeatedScalarUnique = BenchFixtures.benchRepeatedScalarUnique(); - benchRepeatedBytesUnique = BenchFixtures.benchRepeatedBytesUnique(); - benchMap = BenchFixtures.benchMap(); - benchComplexSchema = BenchFixtures.benchComplexSchema(); - benchGT = BenchFixtures.benchGT(); - testByteMatching = BenchFixtures.testByteMatching(); - stringMatching = BenchFixtures.stringMatching(); - wrapperTesting = BenchFixtures.wrapperTesting(); - multiRuleNoError = BenchFixtures.multiRuleNoError(); - multiRuleError = BenchFixtures.multiRuleError(); - benchBoolConst = BenchFixtures.benchBoolConst(); - benchEnumRules = BenchFixtures.benchEnumRules(); - - benchStringConst = BenchFixtures.benchStringConst(); - benchStringLen = BenchFixtures.benchStringLen(); - benchStringMinLen = BenchFixtures.benchStringMinLen(); - benchStringPrefix = BenchFixtures.benchStringPrefix(); - benchStringContains = BenchFixtures.benchStringContains(); - benchStringIn = BenchFixtures.benchStringIn(); - benchBytesConst = BenchFixtures.benchBytesConst(); - benchBytesIn = BenchFixtures.benchBytesIn(); - benchInt64Const = BenchFixtures.benchInt64Const(); - benchInt64In = BenchFixtures.benchInt64In(); - benchUint32In = BenchFixtures.benchUint32In(); - benchDoubleIn = BenchFixtures.benchDoubleIn(); - benchEnumConst = BenchFixtures.benchEnumConst(); - benchEnumNotIn = BenchFixtures.benchEnumNotIn(); - benchRepeatedStringUnique = BenchFixtures.benchRepeatedStringUnique(); - benchRepeatedInt32Unique = BenchFixtures.benchRepeatedInt32Unique(); - - // Warm evaluator cache for steady-state benchmarks. - validator.validate(simple); - validator.validate(manyUnruled); - validator.validate(repeatedRule); - validator.validate(regexPattern); - validator.validate(benchScalar); - validator.validate(benchRepeatedScalar); - validator.validate(benchRepeatedMessage); - validator.validate(benchRepeatedScalarUnique); - validator.validate(benchRepeatedBytesUnique); - validator.validate(benchMap); - validator.validate(benchComplexSchema); - validator.validate(benchGT); - validator.validate(testByteMatching); - validator.validate(stringMatching); - validator.validate(wrapperTesting); - validator.validate(multiRuleNoError); - validator.validate(multiRuleError); - validator.validate(benchBoolConst); - validator.validate(benchEnumRules); - validator.validate(benchStringConst); - validator.validate(benchStringLen); - validator.validate(benchStringMinLen); - validator.validate(benchStringPrefix); - validator.validate(benchStringContains); - validator.validate(benchStringIn); - validator.validate(benchBytesConst); - validator.validate(benchBytesIn); - validator.validate(benchInt64Const); - validator.validate(benchInt64In); - validator.validate(benchUint32In); - validator.validate(benchDoubleIn); - validator.validate(benchEnumConst); - validator.validate(benchEnumNotIn); - validator.validate(benchRepeatedStringUnique); - validator.validate(benchRepeatedInt32Unique); - } - - // --- Existing regression-guard benchmarks --- - - @Benchmark - public void validateSimple(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(simple)); - } - - @Benchmark - public void validateManyUnruled(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(manyUnruled)); - } - - @Benchmark - public void validateRepeatedRule(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(repeatedRule)); - } - - @Benchmark - public void validateRegexPattern(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(regexPattern)); - } - - // --- Native-rules port benchmarks (mirror Go BenchmarkXxx names) --- - - @Benchmark - public void validateBenchScalar(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchScalar)); - } - - @Benchmark - public void validateBenchRepeatedScalar(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchRepeatedScalar)); - } - - @Benchmark - public void validateBenchRepeatedMessage(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchRepeatedMessage)); - } - - @Benchmark - public void validateBenchRepeatedScalarUnique(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchRepeatedScalarUnique)); - } - - @Benchmark - public void validateBenchRepeatedBytesUnique(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchRepeatedBytesUnique)); - } - - @Benchmark - public void validateBenchMap(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchMap)); - } - - @Benchmark - public void validateBenchComplexSchema(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchComplexSchema)); - } - - @Benchmark - public void validateBenchInt32GT(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchGT)); - } - - @Benchmark - public void validateTestByteMatching(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(testByteMatching)); - } - - @Benchmark - public void validateStringMatching(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(stringMatching)); - } - - @Benchmark - public void validateWrapperTesting(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(wrapperTesting)); - } - - @Benchmark - public void validateMultiRuleNoError(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(multiRuleNoError)); - } - - @Benchmark - public void validateMultiRuleError(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(multiRuleError)); - } - - @Benchmark - public void validateBenchBoolConst(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchBoolConst)); - } - - @Benchmark - public void validateBenchEnumRules(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchEnumRules)); - } - - // --- Single-rule fixtures filling earlier coverage gaps --- - - @Benchmark - public void validateBenchStringConst(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchStringConst)); - } - - @Benchmark - public void validateBenchStringLen(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchStringLen)); - } - - @Benchmark - public void validateBenchStringMinLen(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchStringMinLen)); - } - - @Benchmark - public void validateBenchStringPrefix(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchStringPrefix)); - } - - @Benchmark - public void validateBenchStringContains(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchStringContains)); - } - - @Benchmark - public void validateBenchStringIn(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchStringIn)); - } - - @Benchmark - public void validateBenchBytesConst(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchBytesConst)); - } - - @Benchmark - public void validateBenchBytesIn(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchBytesIn)); - } - - @Benchmark - public void validateBenchInt64Const(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchInt64Const)); - } - - @Benchmark - public void validateBenchInt64In(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchInt64In)); - } - - @Benchmark - public void validateBenchUint32In(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchUint32In)); - } - - @Benchmark - public void validateBenchDoubleIn(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchDoubleIn)); - } - - @Benchmark - public void validateBenchEnumConst(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchEnumConst)); - } - - @Benchmark - public void validateBenchEnumNotIn(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchEnumNotIn)); - } - - @Benchmark - public void validateBenchRepeatedStringUnique(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchRepeatedStringUnique)); - } - - @Benchmark - public void validateBenchRepeatedInt32Unique(Blackhole bh) throws ValidationException { - bh.consume(validator.validate(benchRepeatedInt32Unique)); - } -} diff --git a/benchmarks/src/jmh/proto/bench/v1/bench.proto b/benchmarks/src/jmh/proto/bench/v1/bench.proto deleted file mode 100644 index d6fde070e..000000000 --- a/benchmarks/src/jmh/proto/bench/v1/bench.proto +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package bench.v1; - -option java_multiple_files = true; -option java_package = "build.buf.protovalidate.benchmarks.gen"; - -import "buf/validate/validate.proto"; - -// Simple single-field message. Baseline for validate() overhead on a small -// message with one cheap rule. -message SimpleStringMessage { - string email = 1 [(buf.validate.field).string.email = true]; -} - -// One ruled field and many fields without rules. Targets the tautology skip -// in PR #454: before the fix, no-rule fields still ran a FieldEvaluator on -// every validate() call. -message ManyUnruledFieldsMessage { - string non_empty = 1 [(buf.validate.field).string.min_len = 1]; - string f1 = 2; - string f2 = 3; - string f3 = 4; - string f4 = 5; - string f5 = 6; - string f6 = 7; - string f7 = 8; - string f8 = 9; - string f9 = 10; -} - -// Twenty fields that share the same rule shape (min_len on string). Targets -// the AST cache fix in PR #451. The cache key lookup mismatch made -// compileRule rebuild the AST for every field during evaluator construction. -// With many same-shape fields, the saved work is N-1 AST builds. -message RepeatedRuleMessage { - string f01 = 1 [(buf.validate.field).string.min_len = 1]; - string f02 = 2 [(buf.validate.field).string.min_len = 1]; - string f03 = 3 [(buf.validate.field).string.min_len = 1]; - string f04 = 4 [(buf.validate.field).string.min_len = 1]; - string f05 = 5 [(buf.validate.field).string.min_len = 1]; - string f06 = 6 [(buf.validate.field).string.min_len = 1]; - string f07 = 7 [(buf.validate.field).string.min_len = 1]; - string f08 = 8 [(buf.validate.field).string.min_len = 1]; - string f09 = 9 [(buf.validate.field).string.min_len = 1]; - string f10 = 10 [(buf.validate.field).string.min_len = 1]; - string f11 = 11 [(buf.validate.field).string.min_len = 1]; - string f12 = 12 [(buf.validate.field).string.min_len = 1]; - string f13 = 13 [(buf.validate.field).string.min_len = 1]; - string f14 = 14 [(buf.validate.field).string.min_len = 1]; - string f15 = 15 [(buf.validate.field).string.min_len = 1]; - string f16 = 16 [(buf.validate.field).string.min_len = 1]; - string f17 = 17 [(buf.validate.field).string.min_len = 1]; - string f18 = 18 [(buf.validate.field).string.min_len = 1]; - string f19 = 19 [(buf.validate.field).string.min_len = 1]; - string f20 = 20 [(buf.validate.field).string.min_len = 1]; -} - -// Single string field with a string.pattern rule. Targets the regex -// recompile-per-evaluation cost: the CEL runtime's matches() calls -// Pattern.compile on every invocation. -message RegexPatternMessage { - string name = 1 [(buf.validate.field).string = { - pattern: "^[[:alpha:]]+( [[:alpha:]]+)*$" - max_bytes: 256 - }]; -} diff --git a/benchmarks/src/jmh/proto/bench/v1/native_bench.proto b/benchmarks/src/jmh/proto/bench/v1/native_bench.proto deleted file mode 100644 index 57ec69e43..000000000 --- a/benchmarks/src/jmh/proto/bench/v1/native_bench.proto +++ /dev/null @@ -1,330 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Messages ported from protovalidate-go's proto/tests/example/v1/bench.proto -// and proto/tests/example/v1/native_test.proto for the native-rules port. -// gofakeit annotations have been stripped; benchmark fixtures are hand-built -// in BenchFixtures.java. - -syntax = "proto3"; - -package bench.v1; - -import "buf/validate/validate.proto"; -import "google/protobuf/wrappers.proto"; - -option java_multiple_files = true; -option java_package = "build.buf.protovalidate.benchmarks.gen"; - -// --- Messages ported from bench.proto --- - -message BenchScalar { - int32 x = 1 [(buf.validate.field).int32.gt = 0]; -} - -message BenchRepeatedScalar { - repeated int32 x = 1 [(buf.validate.field).repeated.max_items = 10]; -} - -message BenchRepeatedMessage { - repeated BenchScalar x = 1 [(buf.validate.field).repeated.max_items = 10]; -} - -message BenchRepeatedScalarUnique { - repeated float x = 1 [(buf.validate.field).repeated.unique = true]; -} - -message BenchRepeatedBytesUnique { - repeated bytes x = 1 [(buf.validate.field).repeated.unique = true]; -} - -message BenchMap { - map entries = 1 [(buf.validate.field).map.min_pairs = 1]; -} - -message BenchComplexSchema { - string s1 = 1 [(buf.validate.field).string.min_len = 1]; - string s2 = 2 [(buf.validate.field).string.max_len = 100]; - int32 i32 = 3 [(buf.validate.field).int32.gt = 0]; - int64 i64 = 4 [(buf.validate.field).int64.lt = 1000]; - uint32 u32 = 5 [(buf.validate.field).uint32.gte = 1]; - uint64 u64 = 6 [(buf.validate.field).uint64.lte = 1000]; - sint32 si32 = 7 [(buf.validate.field).sint32.gt = 0]; - sint64 si64 = 8 [(buf.validate.field).sint64.lt = 1000]; - fixed32 f32 = 9 [(buf.validate.field).fixed32.gte = 1]; - fixed64 f64 = 10 [(buf.validate.field).fixed64.lte = 1000]; - sfixed32 sf32 = 11 [(buf.validate.field).sfixed32.gt = 0]; - sfixed64 sf64 = 12 [(buf.validate.field).sfixed64.lt = 1000]; - float fl = 13 [(buf.validate.field).float.finite = true]; - double db = 14 [(buf.validate.field).double.finite = true]; - bool bl = 15; - bytes by = 16 [(buf.validate.field).bytes.min_len = 1]; - - BenchScalar nested = 17; - - BenchComplexSchema self_ref = 18; - - repeated string rep_str = 19 [(buf.validate.field).repeated.max_items = 10]; - repeated int32 rep_i32 = 20 [(buf.validate.field).repeated.min_items = 1]; - repeated bytes rep_bytes = 21 [(buf.validate.field).repeated.unique = true]; - - repeated BenchScalar rep_msg = 22 [(buf.validate.field).repeated.max_items = 5]; - - map map_str_str = 23 [(buf.validate.field).map.min_pairs = 1]; - map map_i32_i64 = 24 [(buf.validate.field).map.max_pairs = 10]; - map map_u64_bool = 25; - map map_str_bytes = 26 [(buf.validate.field).map.keys = { - string: {min_len: 1} - }]; - - map map_str_msg = 27 [(buf.validate.field).map.values = {required: true}]; - map map_i64_msg = 28; - - BenchEnum enum_field = 29 [(buf.validate.field).enum.defined_only = true]; - - oneof choice { - string oneof_str = 30 [(buf.validate.field).string.min_len = 1]; - int32 oneof_i32 = 31 [(buf.validate.field).int32.gt = 0]; - BenchScalar oneof_msg = 32; - } -} - -enum BenchEnum { - BENCH_ENUM_UNSPECIFIED = 0; - BENCH_ENUM_ONE = 1; - BENCH_ENUM_TWO = 2; -} - -// --- Messages ported from native_test.proto --- - -message BenchGT { - int32 gt = 1 [(buf.validate.field).int32.gt = 0]; - int32 gte = 2 [(buf.validate.field).int32.gte = 0]; - int32 lt = 3 [(buf.validate.field).int32.lt = 101]; - int32 lte = 4 [(buf.validate.field).int32.lte = 101]; - int32 gtltin = 5 [ - (buf.validate.field).int32.gt = 0, - (buf.validate.field).int32.lt = 101 - ]; - int32 gtltein = 6 [ - (buf.validate.field).int32.gt = 0, - (buf.validate.field).int32.lt = 101 - ]; - int32 gtltex = 7 [ - (buf.validate.field).int32.gt = 0, - (buf.validate.field).int32.lt = -20 - ]; - int32 gtlteex = 8 [ - (buf.validate.field).int32.gt = 0, - (buf.validate.field).int32.lte = -20 - ]; - int32 gteltin = 9 [ - (buf.validate.field).int32.gte = 0, - (buf.validate.field).int32.lt = 101 - ]; - int32 gteltein = 10 [ - (buf.validate.field).int32.gte = 0, - (buf.validate.field).int32.lt = 101 - ]; - int32 gteltex = 11 [ - (buf.validate.field).int32.gte = 0, - (buf.validate.field).int32.lt = -20 - ]; - int32 gtelteex = 12 [ - (buf.validate.field).int32.gte = 0, - (buf.validate.field).int32.lte = -20 - ]; - int32 const = 13 [(buf.validate.field).int32.const = 10]; - int32 constgt = 14 [ - (buf.validate.field).int32.const = 10, - (buf.validate.field).int32.gte = 0 - ]; - int32 in_test = 15 [(buf.validate.field).int32 = { - in: [ - 1, - 3, - 5 - ] - }]; - int32 not_in_test = 16 [(buf.validate.field).int32 = { - not_in: [ - 1, - 3, - 5 - ] - }]; -} - -message TestByteMatching { - bytes ip_addr = 1 [(buf.validate.field).bytes.ip = true]; - bytes ipv4_addr = 2 [(buf.validate.field).bytes.ipv4 = true]; - bytes ipv6_addr = 3 [(buf.validate.field).bytes.ipv6 = true]; - bytes uuid = 4 [(buf.validate.field).bytes.uuid = true]; -} - -message StringMatching { - string hostname = 1 [(buf.validate.field).string.hostname = true]; - string host_and_port = 2 [(buf.validate.field).string.host_and_port = true]; - string email = 3 [(buf.validate.field).string.email = true]; - string uuid = 4 [(buf.validate.field).string.uuid = true]; -} - -message WrapperTesting { - google.protobuf.Int32Value i32 = 1 [(buf.validate.field).int32.gt = 10]; - google.protobuf.DoubleValue d = 2 [(buf.validate.field).double.gt = 10]; - google.protobuf.FloatValue f = 3 [(buf.validate.field).float.gt = 10]; - google.protobuf.Int64Value i64 = 4 [(buf.validate.field).int64.gt = 10]; - google.protobuf.UInt64Value u64 = 5 [(buf.validate.field).uint64.gt = 10]; - google.protobuf.UInt32Value u32 = 6 [(buf.validate.field).uint32.gt = 10]; - google.protobuf.BoolValue b = 7 [(buf.validate.field).bool.const = true]; - google.protobuf.StringValue s = 8 [(buf.validate.field).string.const = "hello"]; - google.protobuf.BytesValue bs = 9 [(buf.validate.field).bytes.len = 5]; -} - -message MultiRule { - int64 many = 1 [ - (buf.validate.field).int64.const = 10, - (buf.validate.field).int64.gt = 5 - ]; -} - -// Phase 2 measurement target. Single bool field with a const rule so the bench has a direct -// hit on BoolRulesEvaluator. -message BenchBoolConst { - bool flag = 1 [(buf.validate.field).bool.const = true]; -} - -enum BenchPhaseEnum { - BENCH_PHASE_ENUM_UNSPECIFIED = 0; - BENCH_PHASE_ENUM_ONE = 1; - BENCH_PHASE_ENUM_TWO = 2; - BENCH_PHASE_ENUM_THREE = 3; -} - -// Phase 4 measurement target. Single enum field with const + in rules so the bench has a direct -// hit on EnumRulesEvaluator. -message BenchEnumRules { - BenchPhaseEnum val = 1 [(buf.validate.field).enum = { - in: [ - 1, - 2, - 3 - ] - }]; -} - -// --- Single-rule fixtures for previously unbenchmarked rules --- -// -// These messages each isolate a single rule so the benchmark measures that rule's per-validation -// cost. They cover gaps identified in the second-pass review: string scalar rules (const, len, -// min_len, prefix, contains, in), bytes scalar rules (const, in), numeric in/const for non-int32 -// kinds, enum const/not_in, and repeated.unique on string/int32. - -message BenchStringConst { - string s = 1 [(buf.validate.field).string.const = "hello"]; -} - -message BenchStringLen { - string s = 1 [(buf.validate.field).string.len = 5]; -} - -message BenchStringMinLen { - string s = 1 [(buf.validate.field).string.min_len = 1]; -} - -message BenchStringPrefix { - string s = 1 [(buf.validate.field).string.prefix = "user-"]; -} - -message BenchStringContains { - string s = 1 [(buf.validate.field).string.contains = "@"]; -} - -message BenchStringIn { - string s = 1 [(buf.validate.field).string = { - in: [ - "foo", - "bar", - "baz" - ] - }]; -} - -message BenchBytesConst { - bytes b = 1 [(buf.validate.field).bytes.const = "abc"]; -} - -message BenchBytesIn { - bytes b = 1 [(buf.validate.field).bytes = { - in: [ - "foo", - "bar" - ] - }]; -} - -message BenchInt64Const { - int64 v = 1 [(buf.validate.field).int64.const = 42]; -} - -message BenchInt64In { - int64 v = 1 [(buf.validate.field).int64 = { - in: [ - 1, - 2, - 3 - ] - }]; -} - -message BenchUint32In { - uint32 v = 1 [(buf.validate.field).uint32 = { - in: [ - 1, - 2, - 3 - ] - }]; -} - -message BenchDoubleIn { - double v = 1 [(buf.validate.field).double = { - in: [ - 1.0, - 2.0, - 3.0 - ] - }]; -} - -message BenchEnumConst { - BenchPhaseEnum val = 1 [(buf.validate.field).enum.const = 1]; -} - -message BenchEnumNotIn { - BenchPhaseEnum val = 1 [(buf.validate.field).enum = { - not_in: [ - 2, - 3 - ] - }]; -} - -message BenchRepeatedStringUnique { - repeated string x = 1 [(buf.validate.field).repeated.unique = true]; -} - -message BenchRepeatedInt32Unique { - repeated int32 x = 1 [(buf.validate.field).repeated.unique = true]; -} \ No newline at end of file diff --git a/buf.gen.yaml b/buf.gen.yaml deleted file mode 100644 index 3cb08a7c7..000000000 --- a/buf.gen.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: v2 -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/sources/bufgen diff --git a/buf.yaml b/buf.yaml deleted file mode 100644 index 3fa55acfe..000000000 --- a/buf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: v2 -modules: - - path: src/main/resources - - path: src/test/resources/proto -lint: - use: - - DEFAULT -breaking: - use: - - FILE diff --git a/build.gradle.kts b/build.gradle.kts index 4255d3a7b..de7e6f79e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,13 @@ +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.SonatypeHost import com.diffplug.gradle.spotless.SpotlessExtension -import com.vanniktech.maven.publish.JavaLibrary -import com.vanniktech.maven.publish.JavadocJar -import com.vanniktech.maven.publish.SourcesJar import net.ltgt.gradle.errorprone.CheckSeverity import net.ltgt.gradle.errorprone.errorprone plugins { + `version-catalog` `java-library` - alias(libs.plugins.errorprone) - alias(libs.plugins.maven) - alias(libs.plugins.osdetector) + alias(libs.plugins.errorprone.plugin) } java { @@ -17,264 +15,10 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } -// The releaseVersion property is set on official releases in the release.yml workflow. -// If not specified, we attempt to calculate a snapshot version based on the last tagged release. -// So if the local build's last tag was v0.1.9, this will set snapshotVersion to 0.1.10-SNAPSHOT. -// If this fails for any reason, we'll fall back to using 0.0.0-SNAPSHOT version. -var snapshotVersion = "0.0.0-SNAPSHOT" -val lastTag = - try { - providers - .exec { - commandLine("git", "describe", "--tags", "--abbrev=0") - isIgnoreExitValue = true - }.standardOutput.asText - .get() - .trim() - } catch (e: Exception) { - "" - } -val matchResult = """^v(\d+)\.(\d+)\.(\d+)$""".toRegex().matchEntire(lastTag) -if (matchResult != null) { - val (major, minor, patch) = matchResult.destructured - snapshotVersion = "$major.$minor.${patch.toInt() + 1}-SNAPSHOT" -} -val releaseVersion = project.findProperty("releaseVersion") as String? ?: snapshotVersion - -val buf: Configuration by configurations.creating -val bufLicenseHeaderCLIFile = - project.layout.buildDirectory - .file("gobin/license-header") - .get() - .asFile -val bufLicenseHeaderCLIPath: String = bufLicenseHeaderCLIFile.absolutePath - -tasks.register("configureBuf") { - description = "Installs the Buf CLI." - File(buf.asPath).setExecutable(true) -} - -tasks.register("installLicenseHeader") { - description = "Installs the Buf license-header CLI." - environment("GOBIN", bufLicenseHeaderCLIFile.parentFile.absolutePath) - inputs.property("bufVersion", libs.versions.buf.get()) - outputs.file(bufLicenseHeaderCLIFile) - commandLine("go", "install", "github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v${libs.versions.buf.get()}") -} - -tasks.register("licenseHeader") { - dependsOn("installLicenseHeader") - description = "Runs the Buf license-header CLI." - commandLine( - bufLicenseHeaderCLIPath, - "--license-type", - "apache", - "--copyright-holder", - "Buf Technologies, Inc.", - "--year-range", - project.findProperty("license-header.years")!!.toString(), - "--ignore", - "build/generated/sources/bufgen/", - "--ignore", - "conformance/build/generated/sources/bufgen/", - "--ignore", - "src/main/resources/buf/validate/", - "--ignore", - ".github", - "--ignore", - "src/test/resources/proto/buf.gen.*.yaml", - "--ignore", - "benchmarks/buf.gen.yaml", - "--ignore", - "benchmarks/buf.yaml", - "--ignore", - "conformance/buf.gen.yaml", - "--ignore", - "conformance/expected-failures.yaml", - "--ignore", - "buf.gen.yaml", - "--ignore", - "buf.yaml", - ) -} - -tasks.register("filterBufGenYaml") { - from(files("buf.gen.yaml")) - from("src/test/resources/proto") { - include("buf*gen*.yaml") - into("src/test/resources/proto") - } - includeEmptyDirs = false - into(layout.buildDirectory.dir("buf-gen-templates")) - expand("protocJavaPluginVersion" to "v${libs.versions.protobuf.get().substringAfter('.')}") - filteringCharset = "UTF-8" -} - -tasks.register("generateTestSourcesImports") { - dependsOn("exportProtovalidateModule", "filterBufGenYaml") - description = "Generates code with buf generate --include-imports for unit tests." - val template = layout.buildDirectory.file("buf-gen-templates/src/test/resources/proto/buf.gen.imports.yaml") - inputs.files(buf) - inputs.file(template) - inputs.dir("src/test/resources/proto") - outputs.dir(layout.buildDirectory.dir("generated/test-sources/bufgen-imports")) - commandLine( - buf.asPath, - "generate", - "--template", - template.get().asFile.absolutePath, - "--include-imports", - ) -} - -tasks.register("generateTestSourcesNoImports") { - dependsOn("exportProtovalidateModule", "filterBufGenYaml") - description = "Generates code with buf generate --include-imports for unit tests." - val template = layout.buildDirectory.file("buf-gen-templates/src/test/resources/proto/buf.gen.noimports.yaml") - inputs.files(buf) - inputs.file(template) - inputs.dir("src/main/resources") - inputs.dir("src/test/resources/proto") - outputs.dir(layout.buildDirectory.dir("generated/test-sources/bufgen-noimports")) - commandLine( - buf.asPath, - "generate", - "--template", - template.get().asFile.absolutePath, - ) -} - -tasks.register("generateCelConformance") { - dependsOn("generateCelConformanceTestTypes", "filterBufGenYaml") - description = "Generates CEL conformance code with buf generate for unit tests." - val template = layout.buildDirectory.file("buf-gen-templates/src/test/resources/proto/buf.gen.cel.yaml") - inputs.files(buf) - inputs.file(template) - inputs.property("celSpecVersion", project.findProperty("cel.spec.version").toString()) - outputs.dir(layout.buildDirectory.dir("generated/test-sources/bufgen-cel")) - commandLine( - buf.asPath, - "generate", - "--template", - template.get().asFile.absolutePath, - "buf.build/google/cel-spec:${project.findProperty("cel.spec.version")}", - "--exclude-path", - "cel/expr/conformance/proto2", - "--exclude-path", - "cel/expr/conformance/proto3", - ) -} - -// The conformance tests use the Protobuf package path for tests that use these types. -// i.e. cel.expr.conformance.proto3.TestAllTypes. But, if we use managed mode it adds 'com' -// to the prefix. Additionally, we can't disable managed mode because the java_package option -// specified in these proto files is "dev.cel.expr.conformance.proto3". So, to get around this, -// we're generating these separately and specifying a java_package override of the package we need. -tasks.register("generateCelConformanceTestTypes") { - dependsOn("exportProtovalidateModule", "filterBufGenYaml") - description = "Generates CEL conformance test types with buf generate for unit tests using a Java package override." - val template = layout.buildDirectory.file("buf-gen-templates/src/test/resources/proto/buf.gen.cel.testtypes.yaml") - inputs.files(buf) - inputs.file(template) - inputs.property("celSpecVersion", project.findProperty("cel.spec.version").toString()) - outputs.dir(layout.buildDirectory.dir("generated/test-sources/bufgen-cel-testtypes")) - commandLine( - buf.asPath, - "generate", - "--template", - template.get().asFile.absolutePath, - "buf.build/google/cel-spec:${project.findProperty("cel.spec.version")}", - "--path", - "cel/expr/conformance/proto3", - ) -} - -var getCelTestData = - tasks.register("getCelTestData") { - val celVersion = project.findProperty("cel.spec.version") - val fileUrl = "https://raw.githubusercontent.com/google/cel-spec/refs/tags/$celVersion/tests/simple/testdata/string_ext.textproto" - val targetDir = File("${project.projectDir}/src/test/resources/testdata") - val file = File(targetDir, "string_ext_$celVersion.textproto") - - onlyIf { - // Only run curl if file doesn't exist - !file.exists() - } - doFirst { - file.parentFile.mkdirs() - commandLine( - "curl", - "-fsSL", - "-o", - file.absolutePath, - fileUrl, - ) - } - } - -tasks.register("generateTestSources") { - dependsOn("generateTestSourcesImports", "generateTestSourcesNoImports", "generateCelConformance") - description = "Generates code with buf generate for unit tests" -} - -val semverRegex = Regex("""^v\d+\.\d+\.\d+(?:-.+)?$""") -tasks.register("exportProtovalidateModule") { - dependsOn("configureBuf") - description = "Exports the bufbuild/protovalidate module sources to src/main/resources." - val version = "${project.findProperty("protovalidate.version")}" - var input = "buf.build/bufbuild/protovalidate:$version" - if (!semverRegex.matches(version)) { - input = "https://github.com/bufbuild/protovalidate.git#subdir=proto/protovalidate,ref=$version" - } - inputs.files(buf) - inputs.property("protovalidateVersion", version) - outputs.dir("src/main/resources/buf") - commandLine( - buf.asPath, - "export", - input, - "--output", - "src/main/resources", - ) -} - -tasks.register("generateSources") { - dependsOn("exportProtovalidateModule", "filterBufGenYaml") - description = "Generates sources for the bufbuild/protovalidate module sources to build/generated/sources/bufgen." - val template = layout.buildDirectory.file("buf-gen-templates/buf.gen.yaml") - inputs.files(buf) - inputs.file(template) - inputs.dir("src/main/resources/buf") - outputs.dir(layout.buildDirectory.dir("generated/sources/bufgen")) - commandLine(buf.asPath, "generate", "--template", template.get().asFile.absolutePath, "src/main/resources") -} - -tasks.register("generate") { - description = "Generates sources with buf generate and buf export." - dependsOn( - "generateTestSources", - "generateSources", - "licenseHeader", - ":conformance:generateConformance", - ":benchmarks:generateBenchmarkSources", - ) -} - -// src/main/resources and build/generated/sources/bufgen are generate-task outputs; consumers need explicit ordering. -tasks.named("processResources") { dependsOn("exportProtovalidateModule") } -tasks.matching { it.name == "sourcesJar" }.configureEach { - dependsOn("exportProtovalidateModule", "generateSources") -} - tasks.withType { - dependsOn("generate") - if (JavaVersion.current().isJava9Compatible) { - doFirst { - options.compilerArgs = mutableListOf("--release", "8") - } + if (JavaVersion.current().isJava9Compatible) doFirst { + options.compilerArgs = mutableListOf("--release", "8") } - // Disable errorprone on generated code - options.errorprone.excludedPaths.set(".*/build/generated/.*") if (!name.lowercase().contains("test")) { options.errorprone { check("NullAway", CheckSeverity.ERROR) @@ -284,10 +28,9 @@ tasks.withType { } tasks.withType { - val stdOptions = options as StandardJavadocDocletOptions - stdOptions.addBooleanOption("Xwerror", true) - // Ignore warnings for generated code. - stdOptions.addBooleanOption("Xdoclint/package:-build.buf.validate,-build.buf.validate.priv", true) + // TODO: Enable when Javadoc changes are final +// val stdOptions = options as StandardJavadocDocletOptions +// stdOptions.addBooleanOption("Xwerror", true) } tasks.withType { @@ -301,128 +44,77 @@ buildscript { } } -sourceSets { - main { - java { - srcDir(layout.buildDirectory.dir("generated/sources/bufgen")) - } - } - test { - java { - srcDir(layout.buildDirectory.dir("generated/test-sources/bufgen-imports")) - srcDir(layout.buildDirectory.dir("generated/test-sources/bufgen-noimports")) - srcDir(layout.buildDirectory.dir("generated/test-sources/bufgen-cel")) - srcDir(layout.buildDirectory.dir("generated/test-sources/bufgen-cel-testtypes")) - } - } -} - -apply(plugin = "com.diffplug.spotless") -configure { - java { - targetExclude("build/generated/sources/bufgen/build/buf/validate/**/*.java", "build/generated/test-sources/bufgen-*/**/*.java") - } - kotlinGradle { - ktlint() - target("**/*.kts") - } -} - allprojects { - version = releaseVersion repositories { mavenCentral() + maven { + name = "buf" + url = uri("https://buf.build/gen/maven") + } } apply(plugin = "com.diffplug.spotless") configure { - isEnforceCheck = false // Disables lint on gradle builds. + setEnforceCheck(false) // Disables lint on gradle builds. java { importOrder() removeUnusedImports() - replaceRegex("Remove wildcard imports", "import\\s+[^\\*\\s]+\\*;(\\r\\n|\\r|\\n)", "$1") googleJavaFormat() endWithNewline() trimTrailingWhitespace() } } - tasks.withType().configureEach { - if (name == "jar") { - manifest { - attributes("Implementation-Version" to releaseVersion) - } - } - } - tasks.withType().configureEach { - dependsOn(getCelTestData) - useJUnitPlatform() - this.testLogging { - events("failed") - exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL - showExceptions = true - showCauses = true - showStackTraces = true - } - } } -mavenPublishing { - val isAutoReleased = project.hasProperty("signingInMemoryKey") - publishToMavenCentral(automaticRelease = true) - if (isAutoReleased) { - signAllPublications() - } - coordinates("build.buf", "protovalidate", releaseVersion) - pomFromGradleProperties() - configure( - JavaLibrary( - // configures the -javadoc artifact, possible values: - // - `JavadocJar.None()` don't publish this artifact - // - `JavadocJar.Empty()` publish an empty jar - // - `JavadocJar.Javadoc()` to publish standard javadocs - javadocJar = JavadocJar.Javadoc(), - sourcesJar = SourcesJar.Sources(), - ), - ) - pom { - name.set("protovalidate-java") - group = "build.buf" - description.set("Protocol Buffer Validation") - url.set("https://github.com/bufbuild/protovalidate-java") - licenses { - license { - name.set("The Apache Software License, Version 2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") - distribution.set("repo") - } - } - developers { - developer { - id.set("bufbuild") - name.set("Buf Technologies") - } +apply(plugin = "com.vanniktech.maven.publish.base") +plugins.withId("com.vanniktech.maven.publish.base") { + configure { + val isAutoReleased = project.hasProperty("signingInMemoryKey") + publishToMavenCentral(SonatypeHost.S01) + if (isAutoReleased) { + signAllPublications() } - scm { + pom { + description.set("Protocol Buffer Validation") + name.set("protovalidate") // This is overwritten in subprojects. + group = "build.buf" + val releaseVersion = project.findProperty("releaseVersion") as String? ?: System.getenv("VERSION") + // Default to snapshot versioning for local publishing. + version = releaseVersion ?: "0.0.0-SNAPSHOT" url.set("https://github.com/bufbuild/protovalidate-java") - connection.set("scm:git:https://github.com/bufbuild/protovalidate-java.git") - developerConnection.set("scm:git:ssh://git@github.com/bufbuild/protovalidate-java.git") + licenses { + license { + name.set("The Apache Software License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + developers { + developer { + id.set("bufbuild") + name.set("Buf Technologies") + } + } + scm { + url.set("https://github.com/bufbuild/protovalidate-java") + connection.set("scm:git:https://github.com/bufbuild/protovalidate-java.git") + developerConnection.set("scm:git:ssh://git@github.com/bufbuild/protovalidate-java.git") + } } } } dependencies { annotationProcessor(libs.nullaway) - api(libs.jspecify) api(libs.protobuf.java) - implementation(libs.cel) - implementation(libs.re2j) - - buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe") + api(libs.protovalidate) + implementation(enforcedPlatform(libs.cel)) + implementation(libs.cel.core) + implementation(libs.guava) + implementation(libs.jakarta.mail.api) testImplementation(libs.assertj) - testImplementation(libs.grpc.protobuf) - testImplementation(platform(libs.junit.bom)) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation(libs.junit) + testImplementation(libs.protovalidate.testing) - errorprone(libs.errorprone.core) -} + errorprone(libs.errorprone) +} \ No newline at end of file diff --git a/conformance/buf.gen.yaml b/conformance/buf.gen.yaml deleted file mode 100644 index 4fecae0eb..000000000 --- a/conformance/buf.gen.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: v2 -managed: - enabled: true - override: - - file_option: java_package_prefix - value: build -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/sources/bufgen diff --git a/conformance/build.gradle.kts b/conformance/build.gradle.kts index 297c6e307..a52130e23 100644 --- a/conformance/build.gradle.kts +++ b/conformance/build.gradle.kts @@ -1,108 +1,8 @@ -import com.diffplug.gradle.spotless.SpotlessExtension -import net.ltgt.gradle.errorprone.errorprone - plugins { `version-catalog` - application java - alias(libs.plugins.errorprone) - alias(libs.plugins.osdetector) -} - -// Conformance tests aren't bound by lowest common library version. -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -val buf: Configuration by configurations.creating - -tasks.register("configureBuf") { - description = "Installs the Buf CLI." - File(buf.asPath).setExecutable(true) -} - -val conformanceCLIFile = - project.layout.buildDirectory - .file("gobin/protovalidate-conformance") - .get() - .asFile -val conformanceCLIPath: String = conformanceCLIFile.absolutePath -val conformanceAppScript: String = - project.layout.buildDirectory - .file("install/conformance/bin/conformance") - .get() - .asFile.absolutePath -val conformanceArgs = (project.findProperty("protovalidate.conformance.args")?.toString() ?: "").split("\\s+".toRegex()) - -tasks.register("installProtovalidateConformance") { - description = "Installs the Protovalidate Conformance CLI." - environment("GOBIN", conformanceCLIFile.parentFile.absolutePath) - inputs.property("protovalidateVersion", project.findProperty("protovalidate.version").toString()) - outputs.file(conformanceCLIFile) - commandLine( - "go", - "install", - "github.com/bufbuild/protovalidate/tools/protovalidate-conformance@${project.findProperty("protovalidate.version")}", - ) -} - -tasks.register("conformance") { - dependsOn("installDist", "installProtovalidateConformance") - description = "Runs protovalidate conformance tests." - commandLine(*(listOf(conformanceCLIPath) + conformanceArgs + listOf(conformanceAppScript)).toTypedArray()) -} - -tasks.register("filterBufGenYaml") { - from(".") - include("buf.gen.yaml") - includeEmptyDirs = false - into(layout.buildDirectory.dir("buf-gen-templates")) - expand("protocJavaPluginVersion" to "v${libs.versions.protobuf.get().substringAfter('.')}") - filteringCharset = "UTF-8" -} - -val semverRegex = Regex("""^v\d+\.\d+\.\d+(?:-.+)?$""") -tasks.register("generateConformance") { - dependsOn("configureBuf", "filterBufGenYaml") - description = "Generates sources for the bufbuild/protovalidate-testing module to build/generated/sources/bufgen." - val version = "${project.findProperty("protovalidate.version")}" - var input = "buf.build/bufbuild/protovalidate-testing:$version" - if (!semverRegex.matches(version)) { - input = "https://github.com/bufbuild/protovalidate.git#subdir=proto/protovalidate-testing,ref=$version" - } - val template = layout.buildDirectory.file("buf-gen-templates/buf.gen.yaml") - inputs.files(buf) - inputs.file(template) - inputs.property("protovalidateVersion", version) - outputs.dir(layout.buildDirectory.dir("generated/sources/bufgen")) - commandLine( - buf.asPath, - "generate", - "--template", - template.get().asFile.absolutePath, - input, - ) -} - -sourceSets { - main { - java { - srcDir(layout.buildDirectory.dir("generated/sources/bufgen")) - } - } -} - -tasks.withType { - dependsOn("generateConformance") - if (JavaVersion.current().isJava9Compatible) { - doFirst { - options.compilerArgs = mutableListOf("--release", "8") - } - } - // Disable errorprone on generated code - options.errorprone.excludedPaths.set(".*/build/generated/sources/bufgen/.*") + alias(libs.plugins.errorprone.plugin) } // Disable javadoc for conformance tests @@ -110,49 +10,35 @@ tasks.withType { enabled = false } -application { - mainClass.set("build.buf.protovalidate.conformance.Main") -} - tasks { jar { - dependsOn(":jar") manifest { - attributes(mapOf("Main-Class" to "build.buf.protovalidate.conformance.Main")) + attributes(mapOf("Main-Class" to "build.buf.Main")) } duplicatesStrategy = DuplicatesStrategy.INCLUDE // This line of code recursively collects and copies all of a project's files // and adds them to the JAR itself. One can extend this task, to skip certain // files or particular types at will val sourcesMain = sourceSets.main.get() - val contents = - configurations.runtimeClasspath - .get() - .map { if (it.isDirectory) it else zipTree(it) } + + val contents = configurations.runtimeClasspath.get() + .map { if (it.isDirectory) it else zipTree(it) } + sourcesMain.output from(contents) } } -apply(plugin = "com.diffplug.spotless") -configure { - java { - targetExclude("build/generated/sources/bufgen/**/*.java") - } -} - dependencies { implementation(project(":")) - implementation(libs.errorprone.annotations) + implementation(libs.guava) implementation(libs.protobuf.java) + implementation(libs.protovalidate) + implementation(libs.protovalidate.testing) + implementation(libs.grpc.netty) + implementation("build.buf.gen:bufbuild_protovalidate-testing_grpc_java:1.56.1.1.00000000000000.79e519aab9ca") - implementation(libs.assertj) - implementation(platform(libs.junit.bom)) - - buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe") + testImplementation(libs.assertj) + testImplementation(libs.junit) + testImplementation(libs.protovalidate.testing) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - - errorprone(libs.errorprone.core) -} + errorprone(libs.errorprone) +} \ No newline at end of file diff --git a/conformance/conformance.sh b/conformance/conformance.sh new file mode 100755 index 000000000..61ee87d40 --- /dev/null +++ b/conformance/conformance.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +java -jar conformance/build/libs/conformance.jar diff --git a/conformance/src/main/java/build/.DS_Store b/conformance/src/main/java/build/.DS_Store deleted file mode 100644 index f17940234..000000000 Binary files a/conformance/src/main/java/build/.DS_Store and /dev/null differ diff --git a/conformance/src/main/java/build/buf/protovalidate/conformance/FileDescriptorUtil.java b/conformance/src/main/java/build/buf/FileDescriptorUtil.java similarity index 59% rename from conformance/src/main/java/build/buf/protovalidate/conformance/FileDescriptorUtil.java rename to conformance/src/main/java/build/buf/FileDescriptorUtil.java index 2f01b236b..34ced95fd 100644 --- a/conformance/src/main/java/build/buf/protovalidate/conformance/FileDescriptorUtil.java +++ b/conformance/src/main/java/build/buf/FileDescriptorUtil.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,13 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate.conformance; +package build.buf; import com.google.protobuf.DescriptorProtos; import com.google.protobuf.Descriptors; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.TypeRegistry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -73,50 +70,4 @@ static Map parseFileDescriptors( } return fileDescriptorMap; } - - static TypeRegistry createTypeRegistry( - Iterable fileDescriptors) { - TypeRegistry.Builder registryBuilder = TypeRegistry.newBuilder(); - for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) { - registryBuilder.add(fileDescriptor.getMessageTypes()); - } - return registryBuilder.build(); - } - - static ExtensionRegistry createExtensionRegistry( - Iterable fileDescriptors) { - ExtensionRegistry registry = ExtensionRegistry.newInstance(); - for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) { - registerFileExtensions(registry, fileDescriptor); - } - return registry; - } - - private static void registerFileExtensions( - ExtensionRegistry registry, Descriptors.FileDescriptor fileDescriptor) { - registerExtensions(registry, fileDescriptor.getExtensions()); - for (Descriptors.Descriptor descriptor : fileDescriptor.getMessageTypes()) { - registerMessageExtensions(registry, descriptor); - } - } - - private static void registerMessageExtensions( - ExtensionRegistry registry, Descriptors.Descriptor descriptor) { - registerExtensions(registry, descriptor.getExtensions()); - for (Descriptors.Descriptor nestedDescriptor : descriptor.getNestedTypes()) { - registerMessageExtensions(registry, nestedDescriptor); - } - } - - private static void registerExtensions( - ExtensionRegistry registry, List extensions) { - for (Descriptors.FieldDescriptor fieldDescriptor : extensions) { - if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) { - registry.add( - fieldDescriptor, DynamicMessage.getDefaultInstance(fieldDescriptor.getMessageType())); - } else { - registry.add(fieldDescriptor); - } - } - } } diff --git a/conformance/src/main/java/build/buf/Main.java b/conformance/src/main/java/build/buf/Main.java new file mode 100644 index 000000000..ebd22a0a9 --- /dev/null +++ b/conformance/src/main/java/build/buf/Main.java @@ -0,0 +1,160 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf; + +import build.buf.gen.buf.validate.ValidateProto; +import build.buf.gen.buf.validate.Violations; +import build.buf.gen.buf.validate.conformance.harness.ConformanceServiceGrpc; +import build.buf.gen.buf.validate.conformance.harness.StreamingConformanceRequest; +import build.buf.gen.buf.validate.conformance.harness.StreamingConformanceResponse; +import build.buf.gen.buf.validate.conformance.harness.TestConformanceRequest; +import build.buf.gen.buf.validate.conformance.harness.TestConformanceResponse; +import build.buf.gen.buf.validate.conformance.harness.TestResult; +import build.buf.protovalidate.Config; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.Validator; +import build.buf.protovalidate.exceptions.CompilationException; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.common.base.Splitter; +import com.google.errorprone.annotations.FormatMethod; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.InvalidProtocolBufferException; +import io.grpc.Server; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.StreamObserver; + +import java.util.List; +import java.util.Map; + +public class Main { + + public static final class Service extends ConformanceServiceGrpc.ConformanceServiceImplBase { + static ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + static { + extensionRegistry.add(ValidateProto.message); + extensionRegistry.add(ValidateProto.field); + extensionRegistry.add(ValidateProto.oneof); + } + + @Override + public void testConformance(TestConformanceRequest request, StreamObserver responseObserver) { + StreamingConformanceResponse streamingConformanceResponse = test(StreamingConformanceRequest.newBuilder() + .setFdset(request.getFdset()) + .putAllCases(request.getCasesMap()) + .build()); + responseObserver.onNext(TestConformanceResponse.newBuilder().putAllResults(streamingConformanceResponse.getResultsMap()).build()); + responseObserver.onCompleted(); + } + + @Override + public StreamObserver streamingConformance(StreamObserver responseObserver) { + return new StreamObserver<>() { + @Override + public void onNext(StreamingConformanceRequest request) { + try { + StreamingConformanceRequest hydratedRequest = StreamingConformanceRequest.parseFrom(request.toByteString(), extensionRegistry); + StreamingConformanceResponse streamingConformanceResponse = test(hydratedRequest); + responseObserver.onNext(streamingConformanceResponse); + } catch (InvalidProtocolBufferException e) { + responseObserver.onError(e); + } + } + + @Override + public void onError(Throwable t) { + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + } + public static void main(String[] args) { + try { + Service bindableService = new Service(); + Server server = NettyServerBuilder.forPort(1235) + .addService(bindableService) + .build(); + server.start(); + server.awaitTermination(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static StreamingConformanceResponse test(StreamingConformanceRequest request) { + try { + Map descriptorMap = + FileDescriptorUtil.parse(request.getFdset()); + Validator validator = new Validator(Config.builder().build()); + StreamingConformanceResponse.Builder responseBuilder = StreamingConformanceResponse.newBuilder().setSuiteName(request.getSuiteName()); + for (Map.Entry entry : request.getCasesMap().entrySet()) { + TestResult testResult = testCase(validator, descriptorMap, entry.getValue()); + responseBuilder.putResults(entry.getKey(), testResult); + } + return responseBuilder.build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static TestResult testCase( + Validator validator, Map fileDescriptors, Any testCase) + throws InvalidProtocolBufferException { + List urlParts = Splitter.on('/').limit(2).splitToList(testCase.getTypeUrl()); + String fullName = urlParts.get(urlParts.size() - 1); + Descriptors.Descriptor descriptor = fileDescriptors.get(fullName); + if (descriptor == null) { + return unexpectedErrorResult("Unable to find descriptor: %s", fullName); + } + ByteString testCaseValue = testCase.getValue(); + DynamicMessage dynamicMessage = + DynamicMessage.newBuilder(descriptor).mergeFrom(testCaseValue).build(); + return validate(validator, dynamicMessage); + } + + private static TestResult validate(Validator validator, DynamicMessage dynamicMessage) { + try { + ValidationResult result = validator.validate(dynamicMessage); + if (result.getViolations().isEmpty()) { + return TestResult.newBuilder().setSuccess(true).build(); + } else { + return TestResult.newBuilder() + .setValidationError( + Violations.newBuilder().addAllViolations(result.getViolations()).build()) + .build(); + } + } catch (CompilationException e) { + return TestResult.newBuilder().setCompilationError(e.getMessage()).build(); + } catch (ExecutionException e) { + return TestResult.newBuilder().setRuntimeError(e.getMessage()).build(); + } catch (Exception e) { + return unexpectedErrorResult("unknown error: %s", e.toString()); + } + } + + @FormatMethod + private static TestResult unexpectedErrorResult(String format, Object... args) { + String errorMessage = String.format(format, args); + return TestResult.newBuilder().setUnexpectedError(errorMessage).build(); + } +} diff --git a/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java b/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java deleted file mode 100644 index 902de0a63..000000000 --- a/conformance/src/main/java/build/buf/protovalidate/conformance/Main.java +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate.conformance; - -import build.buf.protovalidate.Config; -import build.buf.protovalidate.ValidationResult; -import build.buf.protovalidate.Validator; -import build.buf.protovalidate.ValidatorFactory; -import build.buf.protovalidate.exceptions.CompilationException; -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.ValidateProto; -import build.buf.validate.Violations; -import build.buf.validate.conformance.harness.TestConformanceRequest; -import build.buf.validate.conformance.harness.TestConformanceResponse; -import build.buf.validate.conformance.harness.TestResult; -import com.google.errorprone.annotations.FormatMethod; -import com.google.protobuf.Any; -import com.google.protobuf.ByteString; -import com.google.protobuf.Descriptors; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.TypeRegistry; -import java.util.HashMap; -import java.util.Map; - -public class Main { - public static void main(String[] args) { - try { - ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - extensionRegistry.add(ValidateProto.message); - extensionRegistry.add(ValidateProto.field); - extensionRegistry.add(ValidateProto.oneof); - TestConformanceRequest request = - TestConformanceRequest.parseFrom(System.in, extensionRegistry); - TestConformanceResponse response = testConformance(request); - response.writeTo(System.out); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - static TestConformanceResponse testConformance(TestConformanceRequest request) { - try { - Map descriptorMap = - FileDescriptorUtil.parse(request.getFdset()); - Map fileDescriptorMap = - FileDescriptorUtil.parseFileDescriptors(request.getFdset()); - TypeRegistry typeRegistry = FileDescriptorUtil.createTypeRegistry(fileDescriptorMap.values()); - ExtensionRegistry extensionRegistry = - FileDescriptorUtil.createExtensionRegistry(fileDescriptorMap.values()); - String envFlag = System.getenv("ENABLE_NATIVE_RULES"); - Config.Builder cfgBuilder = - Config.newBuilder().setTypeRegistry(typeRegistry).setExtensionRegistry(extensionRegistry); - if (envFlag != null) { - cfgBuilder.setEnableNativeRules(Boolean.parseBoolean(envFlag)); - } - Config cfg = cfgBuilder.build(); - Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); - - TestConformanceResponse.Builder responseBuilder = TestConformanceResponse.newBuilder(); - Map resultsMap = new HashMap<>(); - for (Map.Entry entry : request.getCasesMap().entrySet()) { - TestResult testResult = testCase(validator, descriptorMap, entry.getValue()); - resultsMap.put(entry.getKey(), testResult); - } - responseBuilder.putAllResults(resultsMap); - return responseBuilder.build(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - static TestResult testCase( - Validator validator, Map fileDescriptors, Any testCase) - throws InvalidProtocolBufferException { - String fullName = testCase.getTypeUrl(); - int slash = fullName.indexOf('/'); - if (slash != -1) { - fullName = fullName.substring(slash + 1); - } - Descriptors.Descriptor descriptor = fileDescriptors.get(fullName); - if (descriptor == null) { - return unexpectedErrorResult("Unable to find descriptor: %s", fullName); - } - ByteString testCaseValue = testCase.getValue(); - DynamicMessage dynamicMessage = - DynamicMessage.newBuilder(descriptor).mergeFrom(testCaseValue).build(); - return validate(validator, dynamicMessage); - } - - private static TestResult validate(Validator validator, DynamicMessage dynamicMessage) { - try { - ValidationResult result = validator.validate(dynamicMessage); - if (result.isSuccess()) { - return TestResult.newBuilder().setSuccess(true).build(); - } - Violations error = - Violations.newBuilder().addAllViolations(result.toProto().getViolationsList()).build(); - return TestResult.newBuilder().setValidationError(error).build(); - } catch (CompilationException e) { - return TestResult.newBuilder().setCompilationError(e.getMessage()).build(); - } catch (ExecutionException e) { - return TestResult.newBuilder().setRuntimeError(e.getMessage()).build(); - } catch (Exception e) { - return unexpectedErrorResult("unknown error: %s", e.toString()); - } - } - - @FormatMethod - static TestResult unexpectedErrorResult(String format, Object... args) { - String errorMessage = String.format(format, args); - return TestResult.newBuilder().setUnexpectedError(errorMessage).build(); - } -} diff --git a/conformance/src/test/java/build/buf/MainTest.java b/conformance/src/test/java/build/buf/MainTest.java new file mode 100644 index 000000000..0c4d2b046 --- /dev/null +++ b/conformance/src/test/java/build/buf/MainTest.java @@ -0,0 +1,24 @@ +package build.buf; + + +import build.buf.gen.buf.validate.conformance.harness.ConformanceServiceGrpc; +import build.buf.gen.buf.validate.conformance.harness.TestConformanceRequest; +import build.buf.gen.buf.validate.conformance.harness.TestConformanceResponse; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class MainTest { +// @Test + public void asdf() { + ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 1234) + .usePlaintext() // Use clear text (no encryption) + .build(); + ConformanceServiceGrpc.ConformanceServiceBlockingStub stub = ConformanceServiceGrpc.newBlockingStub(channel); + TestConformanceResponse testConformanceResponse = stub.testConformance(TestConformanceRequest.newBuilder().build()); + assertEquals(testConformanceResponse, testConformanceResponse); + } + +} \ No newline at end of file diff --git a/conformance/src/test/java/build/buf/protovalidate/NativeRulesParityTest.java b/conformance/src/test/java/build/buf/protovalidate/NativeRulesParityTest.java deleted file mode 100644 index 88f6266ca..000000000 --- a/conformance/src/test/java/build/buf/protovalidate/NativeRulesParityTest.java +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.conformance.cases.AnEnum; -import build.buf.validate.conformance.cases.BoolConstTrue; -import build.buf.validate.conformance.cases.BytesContains; -import build.buf.validate.conformance.cases.BytesIn; -import build.buf.validate.conformance.cases.ComplexTestMsg; -import build.buf.validate.conformance.cases.EnumDefined; -import build.buf.validate.conformance.cases.Fixed32LT; -import build.buf.validate.conformance.cases.Int32In; -import build.buf.validate.conformance.cases.KitchenSinkMessage; -import build.buf.validate.conformance.cases.RepeatedEnumIn; -import build.buf.validate.conformance.cases.RepeatedExact; -import build.buf.validate.conformance.cases.RepeatedUnique; -import build.buf.validate.conformance.cases.SFixed64In; -import build.buf.validate.conformance.cases.StringContains; -import build.buf.validate.conformance.cases.StringLen; -import build.buf.validate.conformance.cases.StringPrefix; -import build.buf.validate.conformance.cases.WrapperDouble; -import com.google.protobuf.ByteString; -import com.google.protobuf.DoubleValue; -import com.google.protobuf.Message; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -/** - * Parity test: runs a representative slice of conformance fixtures through both modes ({@code - * enableNativeRules=true} and {@code false}) and asserts the resulting {@code Violation} protos are - * byte-equal. The conformance suite proves each mode is correct in isolation; this test proves they - * don't drift from each other on the same input. - * - *

Conformance message text is excluded from the suite's default comparison (only {@code - * rule_id}, {@code field}, {@code rule}, {@code for_key} are compared unless {@code - * --strict_message} is set), but {@code toProto()} captures all of those plus the message text. - * Asserting full {@code toProto()} equality here is therefore stricter than conformance. - */ -class NativeRulesParityTest { - - private final Validator nativeValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(true).build()) - .build(); - private final Validator celValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - - /** - * Each entry exercises a different rule type. JUnit reports per-fixture pass/fail, so adding a - * fixture and watching CI is a single-line change. Order: bool, enum, bytes, numeric (signed + - * unsigned), string (scalar + format), repeated, map. - */ - static Stream fixtures() { - return Stream.of( - // Bool — fails const=true. - Arguments.of("BoolConstTrue", BoolConstTrue.newBuilder().build()), - // Enum defined_only — fails (2147483647 not in defined values). - Arguments.of( - "EnumDefined.undefined", EnumDefined.newBuilder().setValValue(2147483647).build()), - // Bytes contains — pass case. - Arguments.of( - "BytesContains.pass", - BytesContains.newBuilder().setVal(ByteString.copyFromUtf8("candy bars")).build()), - // Bytes in — pass case (empty matches none of the in list, but the field is - // implicit-presence so the rule is skipped on empty value). - Arguments.of( - "BytesIn.pass", BytesIn.newBuilder().setVal(ByteString.copyFromUtf8("bar")).build()), - // Fixed32 (unsigned) lt — fails (val=5, lt=5). - Arguments.of("Fixed32LT.fail", Fixed32LT.newBuilder().setVal(5).build()), - // Int32 in — fails (4 not in list). - Arguments.of("Int32In.fail", Int32In.newBuilder().setVal(4).build()), - // SFixed64 in — fails (5 not in list). - Arguments.of("SFixed64In.fail", SFixed64In.newBuilder().setVal(5).build()), - // String prefix — pass case. - Arguments.of("StringPrefix.pass", StringPrefix.newBuilder().setVal("foo").build()), - // String contains — pass case. - Arguments.of("StringContains.pass", StringContains.newBuilder().setVal("foobar").build()), - // String length with code points — emoji counts as 1 each. - Arguments.of("StringLen.emoji", StringLen.newBuilder().setVal("😅😄👾").build()), - // Repeated exact — fails (2 items, exact=3). - Arguments.of( - "RepeatedExact.fail", - RepeatedExact.newBuilder().addAllVal(Arrays.asList(1, 2)).build()), - // Repeated unique — fails (duplicate "foo"). - Arguments.of( - "RepeatedUnique.fail", - RepeatedUnique.newBuilder() - .addAllVal(Arrays.asList("foo", "bar", "foo", "baz")) - .build()), - // Repeated enum in — fails. - Arguments.of( - "RepeatedEnumIn.fail", RepeatedEnumIn.newBuilder().addVal(AnEnum.AN_ENUM_X).build()), - // Wrapper-typed double (google.protobuf.DoubleValue) — exercises the native - // wrapper-unwrap path. Empty wrapper = value 0.0, fails the rule. - Arguments.of( - "WrapperDouble.emptyInner", - WrapperDouble.newBuilder().setVal(DoubleValue.newBuilder().build()).build()), - // KitchenSinkMessage with empty inner ComplexTestMsg — many violations. - Arguments.of( - "KitchenSinkMessage.emptyInner", - KitchenSinkMessage.newBuilder().setVal(ComplexTestMsg.newBuilder().build()).build())); - } - - @ParameterizedTest(name = "{0}") - @MethodSource("fixtures") - void parityForFixture(String name, Message msg) throws ValidationException { - ValidationResult nativeResult = nativeValidator.validate(msg); - ValidationResult celResult = celValidator.validate(msg); - assertThat(toProtoList(nativeResult)) - .as("toProto() parity for %s", name) - .isEqualTo(toProtoList(celResult)); - } - - private static List toProtoList(ValidationResult result) { - return result.getViolations().stream().map(Violation::toProto).collect(Collectors.toList()); - } -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index fb1eaea7f..000000000 --- a/gradle.properties +++ /dev/null @@ -1,11 +0,0 @@ -# Version of buf.build/bufbuild/protovalidate to use. -protovalidate.version = v1.2.0 - -# Arguments to the protovalidate-conformance CLI -protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml - -# Argument to the license-header CLI -license-header.years = 2023-2026 - -# Version of the cel-spec that this implementation is conformant with -cel.spec.version = v0.24.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c928b5f6..192ac4d11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,30 +1,26 @@ [versions] -assertj = "3.27.7" -buf = "1.72.0" -cel = "0.13.1" -error-prone = "2.50.0" -junit = "5.14.4" -maven-publish = "0.37.0" -protobuf = "4.35.1" -re2j = "1.8" +assertj = "3.24.2" +cel = "0.3.21" +junit = "4.13.2" +protobuf = "3.23.4" [libraries] assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } -buf = { module = "build.buf:buf", version.ref = "buf" } -cel = { module = "dev.cel:cel", version.ref = "cel" } -errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" } -errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" } -grpc-protobuf = { module = "io.grpc:grpc-protobuf", version = "1.83.1" } -jspecify = { module ="org.jspecify:jspecify", version = "1.0.1" } -junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } -maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } -nullaway = { module = "com.uber.nullaway:nullaway", version = "0.13.8" } +cel = { module = "org.projectnessie.cel:cel-bom", version.ref = "cel" } +cel-core = { module = "org.projectnessie.cel:cel-core" } +errorprone = { module = "com.google.errorprone:error_prone_core", version = "2.20.0" } +guava = { module = "com.google.guava:guava", version = "32.1.1-jre" } +grpc-netty = { module = "io.grpc:grpc-netty-shaded", version = "1.56.1" } +jakarta-mail-api = { module = "jakarta.mail:jakarta.mail-api", version = "2.1.2" } +junit = { module = "junit:junit", version.ref = "junit" } +maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.22.0" } +mail = { module = "javax.mail:mail", version = "1.4.7"} +nullaway = { module = "com.uber.nullaway:nullaway", version = "0.10.11" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } -re2j = { module = "com.google.re2j:re2j", version.ref = "re2j" } -spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "8.9.0" } +protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobuf" } +protovalidate = { module = "build.buf.gen:bufbuild_protovalidate_protocolbuffers_java", version = "23.4.0.1.20230711201221.890d7e3584ce" } +protovalidate-testing = { module = "build.buf.gen:bufbuild_protovalidate-testing_protocolbuffers_java", version = "23.4.0.1.00000000000000.79e519aab9ca" } +spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.13.0" } [plugins] -errorprone = { id = "net.ltgt.errorprone", version = "5.1.0" } -jmh = { id = "me.champeau.jmh", version = "0.7.3" } -maven = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish" } -osdetector = { id = "com.google.osdetector", version = "1.7.3" } +errorprone-plugin = { id = "net.ltgt.errorprone", version = "3.1.0" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index eddabd2ee..249e5832f 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69dd0d040..84a0b92f9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip -networkTimeout=10000 -retries=0 -retryBackOffMs=500 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 249efbb03..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,12 +15,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# SPDX-License-Identifier: Apache-2.0 -# ############################################################################## # -# gradlew start up script for POSIX generated by Gradle. +# Gradle start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +27,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh gradlew +# ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -82,11 +80,13 @@ do esac done -# This is normally unused -# shellcheck disable=SC2034 +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -114,6 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -132,29 +133,22 @@ location of your Java installation." fi else JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -171,6 +165,7 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -198,19 +193,16 @@ if "$cygwin" || "$msys" ; then done fi - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 8508ef684..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,22 +13,19 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem gradlew startup script for Windows +@rem Gradle startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -45,13 +42,13 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. -"%COMSPEC%" /c exit 1 +goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -59,24 +56,36 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. -"%COMSPEC%" /c exit 1 +goto fail :execute @rem Setup the command line +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% -@rem Execute gradlew -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel +:mainEnd +if "%OS%"=="Windows_NT" endlocal -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts index 271e6cf6e..f321a81c3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,2 @@ rootProject.name = "protovalidate" -include("conformance") -include("benchmarks") +include("conformance") \ No newline at end of file diff --git a/src/main/java/build/buf/protovalidate/AnyEvaluator.java b/src/main/java/build/buf/protovalidate/AnyEvaluator.java deleted file mode 100644 index d7d18356d..000000000 --- a/src/main/java/build/buf/protovalidate/AnyEvaluator.java +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.AnyRules; -import build.buf.validate.FieldPath; -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors; -import com.google.protobuf.Message; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * A specialized evaluator for applying {@link build.buf.validate.AnyRules} to an {@link - * com.google.protobuf.Any} message. This is handled outside CEL which attempts to hydrate {@link - * com.google.protobuf.Any}'s within an expression, breaking evaluation if the type is unknown at - * runtime. - */ -final class AnyEvaluator implements Evaluator { - private final RuleViolationHelper helper; - private final Descriptors.FieldDescriptor typeURLDescriptor; - private final Set in; - private final List inValue; - private final Set notIn; - private final List notInValue; - - private static final Descriptors.FieldDescriptor ANY_DESCRIPTOR = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.ANY_FIELD_NUMBER); - - private static final Descriptors.FieldDescriptor IN_DESCRIPTOR = - AnyRules.getDescriptor().findFieldByNumber(AnyRules.IN_FIELD_NUMBER); - - private static final Descriptors.FieldDescriptor NOT_IN_DESCRIPTOR = - AnyRules.getDescriptor().findFieldByNumber(AnyRules.NOT_IN_FIELD_NUMBER); - - private static final FieldPath IN_RULE_PATH = - FieldPath.newBuilder() - .addElements(FieldPathUtils.fieldPathElement(ANY_DESCRIPTOR)) - .addElements(FieldPathUtils.fieldPathElement(IN_DESCRIPTOR)) - .build(); - - private static final FieldPath NOT_IN_RULE_PATH = - FieldPath.newBuilder() - .addElements(FieldPathUtils.fieldPathElement(ANY_DESCRIPTOR)) - .addElements(FieldPathUtils.fieldPathElement(NOT_IN_DESCRIPTOR)) - .build(); - - /** Constructs a new evaluator for {@link build.buf.validate.AnyRules} messages. */ - AnyEvaluator( - ValueEvaluator valueEvaluator, - Descriptors.FieldDescriptor typeURLDescriptor, - List in, - List notIn) { - this.helper = new RuleViolationHelper(valueEvaluator); - this.typeURLDescriptor = typeURLDescriptor; - this.in = stringsToSet(in); - this.inValue = in; - this.notIn = stringsToSet(notIn); - this.notInValue = notIn; - } - - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - Message anyValue = val.messageValue(); - if (anyValue == null) { - return RuleViolation.NO_VIOLATIONS; - } - List violationList = new ArrayList<>(); - String typeURL = (String) anyValue.getField(typeURLDescriptor); - if (!in.isEmpty() && !in.contains(typeURL)) { - RuleViolation.Builder violation = - RuleViolation.newBuilder() - .addAllRulePathElements(helper.getRulePrefixElements()) - .addAllRulePathElements(IN_RULE_PATH.getElementsList()) - .addFirstFieldPathElement(helper.getFieldPathElement()) - .setRuleId("any.in") - .setMessage("type URL must be in the allow list") - .setFieldValue(new RuleViolation.FieldValue(val)) - .setRuleValue(new RuleViolation.FieldValue(this.inValue, IN_DESCRIPTOR)); - violationList.add(violation); - if (failFast) { - return violationList; - } - } - if (!notIn.isEmpty() && notIn.contains(typeURL)) { - RuleViolation.Builder violation = - RuleViolation.newBuilder() - .addAllRulePathElements(helper.getRulePrefixElements()) - .addAllRulePathElements(NOT_IN_RULE_PATH.getElementsList()) - .addFirstFieldPathElement(helper.getFieldPathElement()) - .setRuleId("any.not_in") - .setMessage("type URL must not be in the block list") - .setFieldValue(new RuleViolation.FieldValue(val)) - .setRuleValue(new RuleViolation.FieldValue(this.notInValue, NOT_IN_DESCRIPTOR)); - violationList.add(violation); - } - return violationList; - } - - @Override - public boolean tautology() { - return in.isEmpty() && notIn.isEmpty(); - } - - /** stringsToMap converts a string list to a set for fast lookup. */ - private static Set stringsToSet(List strings) { - if (strings.isEmpty()) { - return Collections.emptySet(); - } - return new HashSet<>(strings); - } -} diff --git a/src/main/java/build/buf/protovalidate/AstExpression.java b/src/main/java/build/buf/protovalidate/AstExpression.java deleted file mode 100644 index 829925b51..000000000 --- a/src/main/java/build/buf/protovalidate/AstExpression.java +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.CompilationException; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelValidationException; -import dev.cel.common.CelValidationResult; -import dev.cel.common.types.CelKind; -import dev.cel.compiler.CelCompiler; - -/** {@link AstExpression} is a compiled CEL {@link CelAbstractSyntaxTree}. */ -final class AstExpression { - /** The compiled CEL AST. */ - final CelAbstractSyntaxTree ast; - - /** Contains the original expression from the proto file. */ - final Expression source; - - /** Constructs a new {@link AstExpression}. */ - private AstExpression(CelAbstractSyntaxTree ast, Expression source) { - this.ast = ast; - this.source = source; - } - - /** - * Compiles the given expression to a {@link AstExpression}. - * - * @param cel The CEL compiler. - * @param expr The expression to compile. - * @return The compiled {@link AstExpression}. - * @throws CompilationException if the expression compilation fails. - */ - static AstExpression newAstExpression(CelCompiler cel, Expression expr) - throws CompilationException { - CelValidationResult compileResult = cel.compile(expr.expression); - if (!compileResult.getAllIssues().isEmpty()) { - throw new CompilationException( - "Failed to compile expression " + expr.id + ":\n" + compileResult.getIssueString()); - } - CelAbstractSyntaxTree ast; - try { - ast = compileResult.getAst(); - } catch (CelValidationException e) { - // This will not happen as we checked for issues, and it only throws when - // it has at least one issue of error severity. - throw new CompilationException( - "Failed to compile expression " + expr.id + ":\n" + compileResult.getIssueString()); - } - CelKind outKind = ast.getResultType().kind(); - if (outKind != CelKind.BOOL && outKind != CelKind.STRING) { - throw new CompilationException( - String.format( - "Expression outputs, wanted either bool or string: %s %s", expr.id, outKind)); - } - return new AstExpression(ast, expr); - } -} diff --git a/src/main/java/build/buf/protovalidate/BoolRulesEvaluator.java b/src/main/java/build/buf/protovalidate/BoolRulesEvaluator.java deleted file mode 100644 index 0ed7430fe..000000000 --- a/src/main/java/build/buf/protovalidate/BoolRulesEvaluator.java +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.BoolRules; -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.Collections; -import java.util.List; -import org.jspecify.annotations.Nullable; - -/** - * Native evaluator for {@code bool} rules. Currently covers {@code bool.const}; the only standard - * rule defined for bool fields. - */ -final class BoolRulesEvaluator implements Evaluator { - private static final FieldDescriptor BOOL_RULES_DESC = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.BOOL_FIELD_NUMBER); - private static final FieldDescriptor CONST_DESC = - BoolRules.getDescriptor().findFieldByNumber(BoolRules.CONST_FIELD_NUMBER); - private static final RuleSite CONST_SITE = - RuleSite.of(BOOL_RULES_DESC, CONST_DESC, "bool.const", null); - - private final RuleBase base; - private final boolean expected; - - private BoolRulesEvaluator(RuleBase base, boolean expected) { - this.base = base; - this.expected = expected; - } - - /** - * Attempts to build a {@link BoolRulesEvaluator} for the bool sub-rules on the given {@code - * FieldRules.Builder}. Returns null if the rules aren't natively handleable (no bool oneof case - * set, no covered rule set, or unknown fields present); on success, clears the covered rule on - * the builder so CEL won't recompile it. - */ - static @Nullable Evaluator tryBuild(RuleBase base, FieldRules.Builder rulesBuilder) { - if (!rulesBuilder.hasBool()) { - return null; - } - BoolRules boolRules = rulesBuilder.getBool(); - if (!boolRules.getUnknownFields().isEmpty()) { - return null; - } - if (!boolRules.hasConst()) { - return null; - } - boolean expected = boolRules.getConst(); - rulesBuilder.setBool(boolRules.toBuilder().clearConst().build()); - return new BoolRulesEvaluator(base, expected); - } - - @Override - public boolean tautology() { - return false; - } - - @Override - public List evaluate(Value val, boolean failFast) { - boolean actual = (Boolean) val.rawValue(); - if (actual == expected) { - return RuleViolation.NO_VIOLATIONS; - } - return base.done( - Collections.singletonList( - NativeViolations.newViolation( - CONST_SITE, null, "must equal " + expected, val, expected))); - } -} diff --git a/src/main/java/build/buf/protovalidate/BytesRulesEvaluator.java b/src/main/java/build/buf/protovalidate/BytesRulesEvaluator.java deleted file mode 100644 index 913df3013..000000000 --- a/src/main/java/build/buf/protovalidate/BytesRulesEvaluator.java +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.BytesRules; -import build.buf.validate.FieldRules; -import com.google.protobuf.ByteString; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.re2j.Pattern; -import com.google.re2j.PatternSyntaxException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.jspecify.annotations.Nullable; - -/** - * Native evaluator for the standard bytes rules: {@code const}, {@code len}, {@code min_len}, - * {@code max_len}, {@code pattern}, {@code prefix}, {@code suffix}, {@code contains}, {@code in}, - * {@code not_in}, plus the well-known size-only formats {@code ip}, {@code ipv4}, {@code ipv6}, - * {@code uuid}. Mirrors {@code nativeBytesEval} in protovalidate-go's {@code native_bytes.go}. - */ -final class BytesRulesEvaluator implements Evaluator { - - /** Well-known bytes format constraint — purely size-based per protovalidate spec. */ - private enum WellKnown { - IP( - "bytes.ip", - "must be a valid IP address", - "bytes.ip_empty", - "value is empty, which is not a valid IP address", - Arrays.asList(4, 16), - BytesRules.IP_FIELD_NUMBER), - IPV4( - "bytes.ipv4", - "must be a valid IPv4 address", - "bytes.ipv4_empty", - "value is empty, which is not a valid IPv4 address", - Collections.singletonList(4), - BytesRules.IPV4_FIELD_NUMBER), - IPV6( - "bytes.ipv6", - "must be a valid IPv6 address", - "bytes.ipv6_empty", - "value is empty, which is not a valid IPv6 address", - Collections.singletonList(16), - BytesRules.IPV6_FIELD_NUMBER), - UUID( - "bytes.uuid", - "must be a valid UUID", - "bytes.uuid_empty", - "value is empty, which is not a valid UUID", - Collections.singletonList(16), - BytesRules.UUID_FIELD_NUMBER); - - final RuleSite site; - final RuleSite emptySite; - final List validSizes; - - WellKnown( - String ruleId, - String message, - String emptyRuleId, - String emptyMessage, - List validSizes, - int fieldNumber) { - FieldDescriptor leaf = BytesRules.getDescriptor().findFieldByNumber(fieldNumber); - this.site = RuleSite.of(BYTES_RULES_DESC, leaf, ruleId, message); - this.emptySite = RuleSite.of(BYTES_RULES_DESC, leaf, emptyRuleId, emptyMessage); - this.validSizes = Collections.unmodifiableList(validSizes); - } - - boolean sizeIsValid(int size) { - return validSizes.contains(size); - } - } - - private static final FieldDescriptor BYTES_RULES_DESC = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.BYTES_FIELD_NUMBER); - - private static final RuleSite CONST_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.CONST_FIELD_NUMBER), - "bytes.const", - null); - private static final RuleSite LEN_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.LEN_FIELD_NUMBER), - "bytes.len", - null); - private static final RuleSite MIN_LEN_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.MIN_LEN_FIELD_NUMBER), - "bytes.min_len", - null); - private static final RuleSite MAX_LEN_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.MAX_LEN_FIELD_NUMBER), - "bytes.max_len", - null); - private static final RuleSite PATTERN_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.PATTERN_FIELD_NUMBER), - "bytes.pattern", - null); - private static final RuleSite PREFIX_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.PREFIX_FIELD_NUMBER), - "bytes.prefix", - null); - private static final RuleSite SUFFIX_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.SUFFIX_FIELD_NUMBER), - "bytes.suffix", - null); - private static final RuleSite CONTAINS_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.CONTAINS_FIELD_NUMBER), - "bytes.contains", - null); - private static final RuleSite IN_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.IN_FIELD_NUMBER), - "bytes.in", - null); - private static final RuleSite NOT_IN_SITE = - RuleSite.of( - BYTES_RULES_DESC, - BytesRules.getDescriptor().findFieldByNumber(BytesRules.NOT_IN_FIELD_NUMBER), - "bytes.not_in", - null); - - private final RuleBase base; - private final @Nullable ByteString constVal; - private final @Nullable Long exactLen; - private final @Nullable Long minLen; - private final @Nullable Long maxLen; - private final @Nullable Pattern pattern; - private final @Nullable String patternStr; - private final @Nullable ByteString prefix; - private final @Nullable ByteString suffix; - private final @Nullable ByteString contains; - private final List inVals; - private final List notInVals; - private final @Nullable WellKnown wellKnown; - - private BytesRulesEvaluator( - RuleBase base, - @Nullable ByteString constVal, - @Nullable Long exactLen, - @Nullable Long minLen, - @Nullable Long maxLen, - @Nullable Pattern pattern, - @Nullable String patternStr, - @Nullable ByteString prefix, - @Nullable ByteString suffix, - @Nullable ByteString contains, - List inVals, - List notInVals, - @Nullable WellKnown wellKnown) { - this.base = base; - this.constVal = constVal; - this.exactLen = exactLen; - this.minLen = minLen; - this.maxLen = maxLen; - this.pattern = pattern; - this.patternStr = patternStr; - this.prefix = prefix; - this.suffix = suffix; - this.contains = contains; - this.inVals = inVals; - this.notInVals = notInVals; - this.wellKnown = wellKnown; - } - - static @Nullable Evaluator tryBuild(RuleBase base, FieldRules.Builder rulesBuilder) { - if (!rulesBuilder.hasBytes()) { - return null; - } - BytesRules rules = rulesBuilder.getBytes(); - if (!rules.getUnknownFields().isEmpty()) { - return null; - } - - BytesRules.Builder bb = rules.toBuilder(); - boolean hasRule = false; - - WellKnown wellKnown = null; - // Mirror Go's switch — earlier cases win. Setting ip=true takes precedence over - // ipv4/ipv6/uuid if multiple are set; protovalidate considers that a misconfiguration but - // we follow Go's order to keep behavior identical. - if (rules.getIp()) { - wellKnown = WellKnown.IP; - bb.clearIp(); - hasRule = true; - } else if (rules.getIpv4()) { - wellKnown = WellKnown.IPV4; - bb.clearIpv4(); - hasRule = true; - } else if (rules.getIpv6()) { - wellKnown = WellKnown.IPV6; - bb.clearIpv6(); - hasRule = true; - } else if (rules.getUuid()) { - wellKnown = WellKnown.UUID; - bb.clearUuid(); - hasRule = true; - } - - ByteString constVal = null; - if (rules.hasConst()) { - constVal = rules.getConst(); - bb.clearConst(); - hasRule = true; - } - - Long exactLen = null; - if (rules.hasLen()) { - exactLen = rules.getLen(); - bb.clearLen(); - hasRule = true; - } - - Long minLen = null; - if (rules.hasMinLen()) { - minLen = rules.getMinLen(); - bb.clearMinLen(); - hasRule = true; - } - - Long maxLen = null; - if (rules.hasMaxLen()) { - maxLen = rules.getMaxLen(); - bb.clearMaxLen(); - hasRule = true; - } - - Pattern compiledPattern = null; - String patternStr = null; - if (rules.hasPattern()) { - patternStr = rules.getPattern(); - try { - compiledPattern = Pattern.compile(patternStr); - } catch (PatternSyntaxException e) { - // Bail to CEL — it produces the same compilation error. - return null; - } - bb.clearPattern(); - hasRule = true; - } - - ByteString prefix = null; - if (rules.hasPrefix()) { - prefix = rules.getPrefix(); - bb.clearPrefix(); - hasRule = true; - } - - ByteString suffix = null; - if (rules.hasSuffix()) { - suffix = rules.getSuffix(); - bb.clearSuffix(); - hasRule = true; - } - - ByteString contains = null; - if (rules.hasContains()) { - contains = rules.getContains(); - bb.clearContains(); - hasRule = true; - } - - // Proto returns immutable views; we only read them. - List inVals = rules.getInList(); - if (!inVals.isEmpty()) { - bb.clearIn(); - hasRule = true; - } - - List notInVals = rules.getNotInList(); - if (!notInVals.isEmpty()) { - bb.clearNotIn(); - hasRule = true; - } - - if (!hasRule) { - return null; - } - rulesBuilder.setBytes(bb.build()); - return new BytesRulesEvaluator( - base, - constVal, - exactLen, - minLen, - maxLen, - compiledPattern, - patternStr, - prefix, - suffix, - contains, - inVals, - notInVals, - wellKnown); - } - - @Override - public boolean tautology() { - return false; - } - - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - ByteString bytesVal = (ByteString) val.rawValue(); - long byteLen = bytesVal.size(); - List violations = null; - - if (constVal != null && !bytesVal.equals(constVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - CONST_SITE, null, "must be " + hex(constVal), val, constVal)); - if (failFast) return base.done(violations); - } - - if (exactLen != null && byteLen != exactLen) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - LEN_SITE, null, "must be " + exactLen + " bytes", val, exactLen)); - if (failFast) return base.done(violations); - } - - if (minLen != null && byteLen < minLen) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MIN_LEN_SITE, null, "must be at least " + minLen + " bytes", val, minLen)); - if (failFast) return base.done(violations); - } - - if (maxLen != null && byteLen > maxLen) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MAX_LEN_SITE, null, "must be at most " + maxLen + " bytes", val, maxLen)); - if (failFast) return base.done(violations); - } - - if (pattern != null) { - if (!bytesVal.isValidUtf8()) { - // Match Go: surface this as an execution error rather than a violation. The conformance - // suite expects pattern checks to fail loudly on non-UTF-8 input. - throw new ExecutionException("must be valid UTF-8 to apply regexp"); - } - if (!pattern.matches(bytesVal.toStringUtf8())) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - PATTERN_SITE, - null, - "must match regex pattern `" + patternStr + "`", - val, - patternStr)); - if (failFast) return base.done(violations); - } - } - - if (prefix != null && !bytesVal.startsWith(prefix)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - PREFIX_SITE, null, "does not have prefix " + hex(prefix), val, prefix)); - if (failFast) return base.done(violations); - } - - if (suffix != null && !bytesVal.endsWith(suffix)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - SUFFIX_SITE, null, "does not have suffix " + hex(suffix), val, suffix)); - if (failFast) return base.done(violations); - } - - if (contains != null && !containsBytes(bytesVal, contains)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - CONTAINS_SITE, null, "does not contain " + hex(contains), val, contains)); - if (failFast) return base.done(violations); - } - - if (!inVals.isEmpty() && !inVals.contains(bytesVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - IN_SITE, null, "must be in list " + formatList(inVals), val, inVals)); - if (failFast) return base.done(violations); - } - - if (!notInVals.isEmpty() && notInVals.contains(bytesVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - NOT_IN_SITE, - null, - "must not be in list " + formatList(notInVals), - val, - notInVals)); - if (failFast) return base.done(violations); - } - - if (wellKnown != null) { - RuleViolation.Builder wkViolation = evaluateWellKnown(bytesVal, val); - if (wkViolation != null) { - violations = RuleBase.add(violations, wkViolation); - if (failFast) return base.done(violations); - } - } - - return base.done(violations); - } - - private RuleViolation.@Nullable Builder evaluateWellKnown(ByteString bytesVal, Value val) { - int size = bytesVal.size(); - WellKnown wk = wellKnown; - if (wk == null) { - return null; - } - if (size == 0) { - // Rule value is the bool 'true' (the rule was enabled). Site has the rule id and message - // pre-baked. - return NativeViolations.newViolation(wk.emptySite, null, null, val, true); - } - if (wk.sizeIsValid(size)) { - return null; - } - return NativeViolations.newViolation(wk.site, null, null, val, true); - } - - /** {@code ByteString} doesn't have a {@code contains} method; implement it directly. */ - private static boolean containsBytes(ByteString haystack, ByteString needle) { - int hLen = haystack.size(); - int nLen = needle.size(); - if (nLen == 0) { - return true; - } - if (nLen > hLen) { - return false; - } - outer: - for (int i = 0; i <= hLen - nLen; i++) { - for (int j = 0; j < nLen; j++) { - if (haystack.byteAt(i + j) != needle.byteAt(j)) { - continue outer; - } - } - return true; - } - return false; - } - - private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); - - /** Lowercase hex encoding to match Go's {@code fmt.Sprintf("%x", ...)} for byte slices. */ - private static String hex(ByteString bs) { - int len = bs.size(); - char[] out = new char[len * 2]; - for (int i = 0; i < len; i++) { - int b = bs.byteAt(i) & 0xff; - out[i * 2] = HEX_DIGITS[b >>> 4]; - out[i * 2 + 1] = HEX_DIGITS[b & 0xf]; - } - return new String(out); - } - - /** - * Formats a list of bytes the way CEL does — each element rendered as its raw string (UTF-8). - * Mirrors Go's {@code formatBytesList}. - */ - private static String formatList(List vals) { - return RuleBase.formatList(vals, ByteString::toStringUtf8); - } -} diff --git a/src/main/java/build/buf/protovalidate/CompiledProgram.java b/src/main/java/build/buf/protovalidate/CompiledProgram.java deleted file mode 100644 index 779b0bd7a..000000000 --- a/src/main/java/build/buf/protovalidate/CompiledProgram.java +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.FieldPath; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime.Program; -import dev.cel.runtime.CelVariableResolver; -import org.jspecify.annotations.Nullable; - -/** - * {@link CompiledProgram} is a parsed and type-checked {@link Program} along with the source {@link - * Expression}. - */ -final class CompiledProgram { - /** A compiled CEL program that can be evaluated against a set of variable bindings. */ - private final Program program; - - /** The original expression that was compiled into the program from the proto file. */ - private final Expression source; - - /** The field path from FieldRules to the rule value. */ - @Nullable private final FieldPath rulePath; - - /** The rule value. */ - @Nullable private final Value ruleValue; - - /** - * Global variables to pass to the evaluation step. Program/CelRuntime doesn't have a concept of - * global variables. - */ - @Nullable private final CelVariableResolver globals; - - /** - * Constructs a new {@link CompiledProgram}. - * - * @param program The compiled CEL program. - * @param source The original expression that was compiled into the program. - * @param rulePath The field path from the FieldRules to the rule value. - * @param ruleValue The rule value. - */ - CompiledProgram( - Program program, - Expression source, - @Nullable FieldPath rulePath, - @Nullable Value ruleValue, - @Nullable CelVariableResolver globals) { - this.program = program; - this.source = source; - this.rulePath = rulePath; - this.ruleValue = ruleValue; - this.globals = globals; - } - - /** - * Evaluate the compiled program with a given set of {@link Variable} variables. - * - * @param variables Variables used for the evaluation. - * @param fieldValue Field value to return in violations. - * @return The {@link build.buf.validate.Violation} from the evaluation, or null if there are no - * violations. - * @throws ExecutionException If the evaluation of the CEL program fails with an error. - */ - RuleViolation.@Nullable Builder eval(Value fieldValue, CelVariableResolver variables) - throws ExecutionException { - Object value; - try { - if (this.globals != null) { - variables = CelVariableResolver.hierarchicalVariableResolver(variables, this.globals); - } - value = program.eval(variables); - } catch (CelEvaluationException e) { - throw new ExecutionException(String.format("error evaluating %s: %s", source.id, e)); - } - if (value instanceof String) { - if ("".equals(value)) { - return null; - } - RuleViolation.Builder builder = - RuleViolation.newBuilder().setRuleId(this.source.id).setMessage(value.toString()); - if (fieldValue.fieldDescriptor() != null) { - builder.setFieldValue(new RuleViolation.FieldValue(fieldValue)); - } - if (rulePath != null) { - builder.addAllRulePathElements(rulePath.getElementsList()); - } - if (ruleValue != null && ruleValue.fieldDescriptor() != null) { - builder.setRuleValue(new RuleViolation.FieldValue(ruleValue)); - } - return builder; - } else if (value instanceof Boolean) { - if (Boolean.TRUE.equals(value)) { - return null; - } - String message = this.source.message; - if (message.isEmpty()) { - message = String.format("\"%s\" returned false", this.source.expression); - } - RuleViolation.Builder builder = - RuleViolation.newBuilder().setRuleId(this.source.id).setMessage(message); - if (rulePath != null) { - builder.addAllRulePathElements(rulePath.getElementsList()); - } - if (ruleValue != null && ruleValue.fieldDescriptor() != null) { - builder.setRuleValue(new RuleViolation.FieldValue(ruleValue)); - } - return builder; - } else { - throw new ExecutionException(String.format("resolved to an unexpected type %s", value)); - } - } -} diff --git a/src/main/java/build/buf/protovalidate/Config.java b/src/main/java/build/buf/protovalidate/Config.java index e12823213..95415aa8b 100644 --- a/src/main/java/build/buf/protovalidate/Config.java +++ b/src/main/java/build/buf/protovalidate/Config.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,32 +14,14 @@ package build.buf.protovalidate; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.TypeRegistry; - /** Config is the configuration for a Validator. */ public final class Config { - private static final TypeRegistry DEFAULT_TYPE_REGISTRY = TypeRegistry.getEmptyTypeRegistry(); - private static final ExtensionRegistry DEFAULT_EXTENSION_REGISTRY = - ExtensionRegistry.getEmptyRegistry(); - private final boolean failFast; - private final TypeRegistry typeRegistry; - private final ExtensionRegistry extensionRegistry; - private final boolean allowUnknownFields; - private final boolean enableNativeRules; + private final boolean disableLazy; - private Config( - boolean failFast, - TypeRegistry typeRegistry, - ExtensionRegistry extensionRegistry, - boolean allowUnknownFields, - boolean enableNativeRules) { + private Config(boolean failFast, boolean disableLazy) { this.failFast = failFast; - this.typeRegistry = typeRegistry; - this.extensionRegistry = extensionRegistry; - this.allowUnknownFields = allowUnknownFields; - this.enableNativeRules = enableNativeRules; + this.disableLazy = disableLazy; } /** @@ -47,7 +29,7 @@ private Config( * * @return a new Configuration builder. */ - public static Builder newBuilder() { + public static Builder builder() { return new Builder(); } @@ -61,53 +43,18 @@ public boolean isFailFast() { } /** - * Gets the type registry used for reparsing protobuf messages. - * - * @return a type registry - */ - public TypeRegistry getTypeRegistry() { - return typeRegistry; - } - - /** - * Gets the extension registry used for resolving unknown protobuf extensions. - * - * @return an extension registry - */ - public ExtensionRegistry getExtensionRegistry() { - return extensionRegistry; - } - - /** - * Checks if the configuration for allowing unknown rule fields is enabled. - * - * @return if allowing unknown rule fields is enabled - */ - public boolean isAllowingUnknownFields() { - return allowUnknownFields; - } - - /** - * Checks whether native (non-CEL) rule evaluators are enabled. - * - *

When true, standard rules with a native Java implementation bypass CEL evaluation. When - * false, all rules go through CEL. Defaults to true; applications opt out by calling {@link - * Builder#setEnableNativeRules(boolean) setEnableNativeRules(false)}. + * Checks if the configuration for disabling lazy evaluation is enabled. * - * @return true if native rules are enabled. + * @return if disabling lazy evaluation is enabled */ - public boolean isNativeRulesEnabled() { - return enableNativeRules; + public boolean isDisableLazy() { + return disableLazy; } /** Builder for configuration. Provides a forward compatible API for users. */ public static final class Builder { private boolean failFast; - private TypeRegistry typeRegistry = DEFAULT_TYPE_REGISTRY; - private ExtensionRegistry extensionRegistry = DEFAULT_EXTENSION_REGISTRY; - private boolean allowUnknownFields; - // native rules are enabled by default - private boolean enableNativeRules = true; + private boolean disableLazy; private Builder() {} @@ -123,68 +70,13 @@ public Builder setFailFast(boolean failFast) { } /** - * Set the type registry for reparsing protobuf messages. This option should be set alongside - * setExtensionRegistry to allow dynamic resolution of predefined rule extensions. It should be - * set to a TypeRegistry with all the message types from your file descriptor set registered. By - * default, if any unknown field rules are found, compilation of the rules will fail; use - * setAllowUnknownFields to control this behavior. - * - *

Note that the message types for any extensions in setExtensionRegistry must be present in - * the typeRegistry, and have an exactly-equal Descriptor. If the type registry is not set, the - * extension types in the extension registry must have exactly-equal Descriptor types to the - * protovalidate built-in messages. If these conditions are not met, extensions will not be - * resolved as expected. These conditions will be met when constructing a TypeRegistry and - * ExtensionRegistry using information from the same file descriptor sets. - * - * @param typeRegistry the type registry to use - * @return this builder - */ - public Builder setTypeRegistry(TypeRegistry typeRegistry) { - this.typeRegistry = typeRegistry; - return this; - } - - /** - * Set the extension registry for resolving unknown extensions. This option should be set - * alongside setTypeRegistry to allow dynamic resolution of predefined rule extensions. It - * should be set to an ExtensionRegistry with all the extension types from your file descriptor - * set registered. By default, if any unknown field rules are found, compilation of the rules - * will fail; use setAllowUnknownFields to control this behavior. - * - * @param extensionRegistry the extension registry to use - * @return this builder - */ - public Builder setExtensionRegistry(ExtensionRegistry extensionRegistry) { - this.extensionRegistry = extensionRegistry; - return this; - } - - /** - * Set whether unknown rule fields are allowed. If this setting is set to true, unknown standard - * predefined field rules and predefined field rule extensions will be ignored. This setting - * defaults to false, which will result in a CompilationException being thrown whenever an - * unknown field rule is encountered. Setting this to true will cause some field rules to be - * ignored; if the descriptor is dynamic, you can instead use setExtensionRegistry to provide - * dynamic type information that protovalidate can use to resolve the unknown fields. - * - * @param allowUnknownFields setting to apply - * @return this builder - */ - public Builder setAllowUnknownFields(boolean allowUnknownFields) { - this.allowUnknownFields = allowUnknownFields; - return this; - } - - /** - * Enables or disables native (non-CEL) rule evaluators. Native rules are enabled by default. - * Forward-compatible: any rule not yet implemented natively continues to be enforced via CEL - * regardless of this setting. + * Set the configuration for disabling lazy evaluation. * - * @param enableNativeRules whether to enable native rules + * @param disableLazy the boolean for enabling * @return this builder */ - public Builder setEnableNativeRules(boolean enableNativeRules) { - this.enableNativeRules = enableNativeRules; + public Builder setDisableLazy(boolean disableLazy) { + this.disableLazy = disableLazy; return this; } @@ -194,8 +86,7 @@ public Builder setEnableNativeRules(boolean enableNativeRules) { * @return the configuration. */ public Config build() { - return new Config( - failFast, typeRegistry, extensionRegistry, allowUnknownFields, enableNativeRules); + return new Config(failFast, disableLazy); } } } diff --git a/src/main/java/build/buf/protovalidate/CustomDeclarations.java b/src/main/java/build/buf/protovalidate/CustomDeclarations.java deleted file mode 100644 index a4f61bf8b..000000000 --- a/src/main/java/build/buf/protovalidate/CustomDeclarations.java +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; -import static dev.cel.common.CelOverloadDecl.newGlobalOverload; -import static dev.cel.common.CelOverloadDecl.newMemberOverload; - -import dev.cel.common.CelFunctionDecl; -import dev.cel.common.CelOverloadDecl; -import dev.cel.common.types.CelType; -import dev.cel.common.types.ListType; -import dev.cel.common.types.SimpleType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -/** Defines custom declaration functions. */ -final class CustomDeclarations { - - /** - * Create the custom function declaration list. - * - * @return the list of function declarations. - */ - static List create() { - List decls = new ArrayList<>(); - - // Add 'getField' function declaration - decls.add( - newFunctionDeclaration( - "getField", - newGlobalOverload( - "get_field_any_string", - SimpleType.DYN, - Arrays.asList(SimpleType.ANY, SimpleType.STRING)))); - // Add 'isIp' function declaration - decls.add( - newFunctionDeclaration( - "isIp", - newMemberOverload( - "is_ip", SimpleType.BOOL, Arrays.asList(SimpleType.STRING, SimpleType.INT)), - newMemberOverload( - "is_ip_unary", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); - - // Add 'isIpPrefix' function declaration - decls.add( - newFunctionDeclaration( - "isIpPrefix", - newMemberOverload( - "is_ip_prefix_int_bool", - SimpleType.BOOL, - Arrays.asList(SimpleType.STRING, SimpleType.INT, SimpleType.BOOL)), - newMemberOverload( - "is_ip_prefix_int", - SimpleType.BOOL, - Arrays.asList(SimpleType.STRING, SimpleType.INT)), - newMemberOverload( - "is_ip_prefix_bool", - SimpleType.BOOL, - Arrays.asList(SimpleType.STRING, SimpleType.BOOL)), - newMemberOverload( - "is_ip_prefix", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); - - // Add 'isUriRef' function declaration - decls.add( - newFunctionDeclaration( - "isUriRef", - newMemberOverload( - "is_uri_ref", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); - - // Add 'isUri' function declaration - decls.add( - newFunctionDeclaration( - "isUri", - newMemberOverload( - "is_uri", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); - - // Add 'isEmail' function declaration - decls.add( - newFunctionDeclaration( - "isEmail", - newMemberOverload( - "is_email", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); - - // Add 'isHostname' function declaration - decls.add( - newFunctionDeclaration( - "isHostname", - newMemberOverload( - "is_hostname", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); - - // Redeclare 'matches' with the same overload ids as the stdlib. - decls.add( - newFunctionDeclaration( - "matches", - newGlobalOverload( - "matches", SimpleType.BOOL, Arrays.asList(SimpleType.STRING, SimpleType.STRING)), - newMemberOverload( - "matches_string", - SimpleType.BOOL, - Arrays.asList(SimpleType.STRING, SimpleType.STRING)))); - - decls.add( - newFunctionDeclaration( - "isHostAndPort", - newMemberOverload( - "string_bool_is_host_and_port_bool", - SimpleType.BOOL, - Arrays.asList(SimpleType.STRING, SimpleType.BOOL)))); - - // Add 'startsWith' function declaration - decls.add( - newFunctionDeclaration( - "startsWith", - newMemberOverload( - "starts_with_bytes", - SimpleType.BOOL, - Arrays.asList(SimpleType.BYTES, SimpleType.BYTES)))); - - // Add 'endsWith' function declaration - decls.add( - newFunctionDeclaration( - "endsWith", - newMemberOverload( - "ends_with_bytes", - SimpleType.BOOL, - Arrays.asList(SimpleType.BYTES, SimpleType.BYTES)))); - - // Add 'contains' function declaration - decls.add( - newFunctionDeclaration( - "contains", - newMemberOverload( - "contains_bytes", - SimpleType.BOOL, - Arrays.asList(SimpleType.BYTES, SimpleType.BYTES)))); - - // Add 'isNan' function declaration - decls.add( - newFunctionDeclaration( - "isNan", - newMemberOverload( - "is_nan", SimpleType.BOOL, Collections.singletonList(SimpleType.DOUBLE)))); - - // Add 'isInf' function declaration - decls.add( - newFunctionDeclaration( - "isInf", - newMemberOverload( - "is_inf_unary", SimpleType.BOOL, Collections.singletonList(SimpleType.DOUBLE)), - newMemberOverload( - "is_inf_binary", - SimpleType.BOOL, - Arrays.asList(SimpleType.DOUBLE, SimpleType.INT)))); - - // Add 'unique' function declaration - List uniqueOverloads = new ArrayList<>(); - for (CelType type : - Arrays.asList( - SimpleType.STRING, - SimpleType.INT, - SimpleType.UINT, - SimpleType.DOUBLE, - SimpleType.BYTES, - SimpleType.BOOL)) { - uniqueOverloads.add( - newMemberOverload( - String.format("unique_list_%s", type.name().toLowerCase(Locale.US)), - SimpleType.BOOL, - Collections.singletonList(ListType.create(type)))); - } - decls.add(newFunctionDeclaration("unique", uniqueOverloads)); - - // Add 'format' function declaration - List formatOverloads = new ArrayList<>(); - formatOverloads.add( - newMemberOverload( - "format_list_dyn", - SimpleType.STRING, - Arrays.asList(SimpleType.STRING, ListType.create(SimpleType.DYN)))); - - decls.add(newFunctionDeclaration("format", formatOverloads)); - - return Collections.unmodifiableList(decls); - } -} diff --git a/src/main/java/build/buf/protovalidate/CustomOverload.java b/src/main/java/build/buf/protovalidate/CustomOverload.java deleted file mode 100644 index 197fcfd42..000000000 --- a/src/main/java/build/buf/protovalidate/CustomOverload.java +++ /dev/null @@ -1,665 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import com.google.protobuf.Descriptors; -import com.google.protobuf.Message; -import com.google.re2j.Matcher; -import com.google.re2j.Pattern; -import com.google.re2j.PatternSyntaxException; -import dev.cel.common.CelOptions; -import dev.cel.common.types.CelType; -import dev.cel.common.types.SimpleType; -import dev.cel.common.values.CelByteString; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelFunctionBinding; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.ConcurrentMap; - -/** Defines custom function overloads (the implementation). */ -final class CustomOverload { - - // See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address - private static final Pattern EMAIL_REGEX = - Pattern.compile( - "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"); - - /** - * Create a list of custom function overloads. - * - * @param patternCache cache used by the {@code matches}/{@code matches_string} overrides. - * @param celOptions CEL options the enclosing runtime is built with. - * @return a list of overloaded functions. - */ - static List create( - ConcurrentMap patternCache, CelOptions celOptions) { - ArrayList bindings = new ArrayList<>(); - bindings.addAll( - Arrays.asList( - celGetField(), - celFormat(), - celStartsWithBytes(), - celEndsWithBytes(), - celContainsBytes(), - celIsHostname(), - celIsEmail(), - celIsIpUnary(), - celIsIp(), - celIsIpPrefix(), - celIsIpPrefixInt(), - celIsIpPrefixBool(), - celIsIpPrefixIntBool(), - celIsUri(), - celIsUriRef(), - celIsNan(), - celIsInfUnary(), - celIsInfBinary(), - celIsHostAndPort(), - celMatches(patternCache, celOptions), - celMatchesString(patternCache, celOptions))); - bindings.addAll(celUnique()); - return Collections.unmodifiableList(bindings); - } - - /** - * Creates a custom function overload for the "getField" operation. - * - * @return The {@link CelFunctionBinding} instance for the "getField" operation. - */ - private static CelFunctionBinding celGetField() { - return CelFunctionBinding.from( - "get_field_any_string", - Message.class, - String.class, - (message, fieldName) -> { - Descriptors.FieldDescriptor field = - message.getDescriptorForType().findFieldByName(fieldName); - if (field == null) { - throw new CelEvaluationException("no such field: " + fieldName); - } - return ProtoAdapter.toCel(field, message.getField(field)); - }); - } - - /** - * Creates a custom binary function overload for the "format" operation. - * - * @return The {@link CelFunctionBinding} instance for the "format" operation. - */ - private static CelFunctionBinding celFormat() { - return CelFunctionBinding.from("format_list_dyn", String.class, List.class, Format::format); - } - - /** - * Creates a custom unary function overload for the "unique" operation. - * - * @return The {@link CelFunctionBinding} instance for the "unique" operation. - */ - private static List celUnique() { - List uniqueOverloads = new ArrayList<>(); - for (CelType type : - Arrays.asList( - SimpleType.STRING, - SimpleType.INT, - SimpleType.UINT, - SimpleType.DOUBLE, - SimpleType.BYTES, - SimpleType.BOOL)) { - uniqueOverloads.add( - CelFunctionBinding.from( - String.format("unique_list_%s", type.name().toLowerCase(Locale.US)), - List.class, - CustomOverload::uniqueList)); - } - return Collections.unmodifiableList(uniqueOverloads); - } - - /** - * Creates a custom binary function overload for the "startsWith" operation. - * - * @return The {@link CelFunctionBinding} instance for the "startsWith" operation. - */ - private static CelFunctionBinding celStartsWithBytes() { - return CelFunctionBinding.from( - "starts_with_bytes", - CelByteString.class, - CelByteString.class, - (receiver, param) -> { - if (receiver.size() < param.size()) { - return false; - } - byte[] paramBytes = param.toByteArray(); - byte[] receiverBytes = receiver.toByteArray(); - for (int i = 0; i < paramBytes.length; i++) { - if (paramBytes[i] != receiverBytes[i]) { - return false; - } - } - return true; - }); - } - - /** - * Creates a custom binary function overload for the "endsWith" operation. - * - * @return The {@link CelFunctionBinding} instance for the "endsWith" operation. - */ - private static CelFunctionBinding celEndsWithBytes() { - return CelFunctionBinding.from( - "ends_with_bytes", - CelByteString.class, - CelByteString.class, - (receiver, param) -> { - if (receiver.size() < param.size()) { - return false; - } - byte[] paramBytes = param.toByteArray(); - byte[] receiverBytes = receiver.toByteArray(); - for (int i = 0; i < paramBytes.length; i++) { - if (paramBytes[param.size() - i - 1] != receiverBytes[receiver.size() - i - 1]) { - return false; - } - } - return true; - }); - } - - /** - * Creates a custom binary function overload for the "contains" operation. - * - * @return The {@link CelFunctionBinding} instance for the "contains" operation. - */ - private static CelFunctionBinding celContainsBytes() { - return CelFunctionBinding.from( - "contains_bytes", - CelByteString.class, - CelByteString.class, - (receiver, param) -> bytesContains(receiver.toByteArray(), param.toByteArray())); - } - - static boolean bytesContains(byte[] arr, byte[] subArr) { - if (subArr.length == 0) { - return true; - } - if (subArr.length > arr.length) { - return false; - } - for (int i = 0; i < arr.length - subArr.length + 1; i++) { - boolean found = true; - for (int j = 0; j < subArr.length; j++) { - if (arr[i + j] != subArr[j]) { - found = false; - break; - } - } - if (found) { - return true; - } - } - return false; - } - - /** - * Creates a custom binary function overload for the "isHostname" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isHostname" operation. - */ - private static CelFunctionBinding celIsHostname() { - return CelFunctionBinding.from("is_hostname", String.class, CustomOverload::isHostname); - } - - /** - * Creates a custom unary function overload for the "isEmail" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isEmail" operation. - */ - private static CelFunctionBinding celIsEmail() { - return CelFunctionBinding.from("is_email", String.class, CustomOverload::isEmail); - } - - /** - * Creates a custom function overload for the "isIp" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isIp" operation. - */ - private static CelFunctionBinding celIsIpUnary() { - return CelFunctionBinding.from("is_ip_unary", String.class, value -> isIp(value, 0L)); - } - - /** - * Creates a custom function overload for the "isIp" operation that also accepts a port. - * - * @return The {@link CelFunctionBinding} instance for the "isIp" operation. - */ - private static CelFunctionBinding celIsIp() { - return CelFunctionBinding.from("is_ip", String.class, Long.class, CustomOverload::isIp); - } - - /** - * Creates a custom function overload for the "isIpPrefix" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isIpPrefix" operation. - */ - private static CelFunctionBinding celIsIpPrefix() { - return CelFunctionBinding.from( - "is_ip_prefix", String.class, prefix -> isIpPrefix(prefix, 0L, false)); - } - - /** - * Creates a custom function overload for the "isIpPrefix" operation that accepts a version. - * - * @return The {@link CelFunctionBinding} instance for the "isIpPrefix" operation. - */ - private static CelFunctionBinding celIsIpPrefixInt() { - return CelFunctionBinding.from( - "is_ip_prefix_int", - String.class, - Long.class, - (prefix, version) -> isIpPrefix(prefix, version, false)); - } - - /** - * Creates a custom function overload for the "isIpPrefix" operation that accepts a strict flag. - * - * @return The {@link CelFunctionBinding} instance for the "isIpPrefix" operation. - */ - private static CelFunctionBinding celIsIpPrefixBool() { - return CelFunctionBinding.from( - "is_ip_prefix_bool", - String.class, - Boolean.class, - (prefix, strict) -> isIpPrefix(prefix, 0L, strict)); - } - - /** - * Creates a custom function overload for the "isIpPrefix" operation that accepts both version and - * strict flag. - * - * @return The {@link CelFunctionBinding} instance for the "isIpPrefix" operation. - */ - private static CelFunctionBinding celIsIpPrefixIntBool() { - return CelFunctionBinding.from( - "is_ip_prefix_int_bool", - Arrays.asList(String.class, Long.class, Boolean.class), - (args) -> isIpPrefix((String) args[0], (Long) args[1], (Boolean) args[2])); - } - - /** - * Creates a custom unary function overload for the "isUri" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isUri" operation. - */ - private static CelFunctionBinding celIsUri() { - return CelFunctionBinding.from("is_uri", String.class, CustomOverload::isUri); - } - - /** - * Creates a custom unary function overload for the "isUriRef" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isUriRef" operation. - */ - private static CelFunctionBinding celIsUriRef() { - return CelFunctionBinding.from("is_uri_ref", String.class, CustomOverload::isUriRef); - } - - /** - * Creates a custom unary function overload for the "isNan" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isNan" operation. - */ - private static CelFunctionBinding celIsNan() { - return CelFunctionBinding.from("is_nan", Double.class, value -> Double.isNaN(value)); - } - - /** - * Creates a custom unary function overload for the "isInf" operation. - * - * @return The {@link CelFunctionBinding} instance for the "isInf" operation. - */ - private static CelFunctionBinding celIsInfUnary() { - return CelFunctionBinding.from("is_inf_unary", Double.class, value -> value.isInfinite()); - } - - /** - * Creates a custom unary function overload for the "isInf" operation with sign option. - * - * @return The {@link CelFunctionBinding} instance for the "isInf" operation. - */ - private static CelFunctionBinding celIsInfBinary() { - return CelFunctionBinding.from( - "is_inf_binary", - Double.class, - Long.class, - (value, sign) -> { - if (sign == 0) { - return value.isInfinite(); - } - double expectedValue = (sign > 0) ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; - return value == expectedValue; - }); - } - - private static CelFunctionBinding celIsHostAndPort() { - return CelFunctionBinding.from( - "string_bool_is_host_and_port_bool", - String.class, - Boolean.class, - CustomOverload::isHostAndPort); - } - - /** Caching replacement for CEL's global {@code matches(string, string)}. */ - @SuppressWarnings("Immutable") - private static CelFunctionBinding celMatches( - ConcurrentMap patternCache, CelOptions celOptions) { - return CelFunctionBinding.from( - "matches", - String.class, - String.class, - (value, regex) -> matches(patternCache, celOptions, value, regex)); - } - - /** Caching replacement for CEL's member-style {@code string.matches(string)}. */ - @SuppressWarnings("Immutable") - private static CelFunctionBinding celMatchesString( - ConcurrentMap patternCache, CelOptions celOptions) { - return CelFunctionBinding.from( - "matches_string", - String.class, - String.class, - (value, regex) -> matches(patternCache, celOptions, value, regex)); - } - - private static boolean matches( - ConcurrentMap cache, CelOptions celOptions, String value, String regex) - throws CelEvaluationException { - Pattern pattern; - try { - pattern = cache.computeIfAbsent(regex, Pattern::compile); - } catch (PatternSyntaxException e) { - throw new CelEvaluationException("failed to compile regex: " + e.getMessage(), e); - } - Matcher matcher = pattern.matcher(value); - return celOptions.enableRegexPartialMatch() ? matcher.find() : matcher.matches(); - } - - /** - * Returns true if the string is a valid host/port pair, for example "example.com:8080". - * - *

If the argument portRequired is true, the port is required. If the argument is false, the - * port is optional. - * - *

The host can be one of: - * - *

    - *
  • An IPv4 address in dotted decimal format, for example {@code 192.168.0.1}. - *
  • An IPv6 address enclosed in square brackets, for example {@code [::1]}. - *
  • A hostname, for example {@code example.com}. - *
- * - *

The port is separated by a colon. It must be non-empty, with a decimal number in the range - * of 0-65535, inclusive. - */ - static boolean isHostAndPort(String str, boolean portRequired) { - if (str.isEmpty()) { - return false; - } - - int splitIdx = str.lastIndexOf(':'); - - if (str.charAt(0) == '[') { - int end = str.lastIndexOf(']'); - - int endPlus = end + 1; - if (endPlus == str.length()) { // no port - return !portRequired && isIp(str.substring(1, end), 6); - } else if (endPlus == splitIdx) { // port - return isIp(str.substring(1, end), 6) && isPort(str.substring(splitIdx + 1)); - } - return false; // malformed - } - - if (splitIdx < 0) { - return !portRequired && (isHostname(str) || isIp(str, 4)); - } - - String host = str.substring(0, splitIdx); - String port = str.substring(splitIdx + 1); - - return ((isHostname(host) || isIp(host, 4)) && isPort(port)); - } - - // Returns true if the string is a valid port for isHostAndPort. - private static boolean isPort(String str) { - if (str.isEmpty()) { - return false; - } - - if (str.length() > 1 && str.charAt(0) == '0') { - return false; - } - - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - if ('0' <= c && c <= '9') { - continue; - } - return false; - } - - try { - int val = Integer.parseInt(str); - return val <= 65535; - } catch (NumberFormatException nfe) { - return false; - } - } - - /** - * Determines if the input list contains unique values. If the list contains duplicate values, it - * returns {@code false}. If the list contains unique values, it returns {@code true}. - * - * @param list The input list to check for uniqueness. - * @return {@code true} if the list contains unique scalar values, {@code false} otherwise. - */ - static boolean uniqueList(List list) { - long size = list.size(); - if (size == 0) { - return true; - } - Set exist = new HashSet<>((int) size); - for (Object val : list) { - // NaN corner case, ignore NaN, because each one should be treated as unique - // also handle 0 and -0 being equal to each other - if (val instanceof Double) { - Double valDouble = (Double) val; - if (valDouble.isNaN()) { - continue; - } - // should normalize 0 and -0 - if (valDouble == 0.0) { - val = 0.0; - } - } - if (val instanceof Float) { - Float valFloat = (Float) val; - if (valFloat.isNaN()) { - continue; - } - // should normalize 0 and -0 - if (valFloat == 0.0f) { - val = 0.0f; - } - } - if (!exist.add(val)) { - return false; - } - } - return true; - } - - /** - * isEmail returns true if addr is a valid email address. - * - *

This regex conforms to the definition for a valid email address from the HTML standard. Note - * that this standard willfully deviates from RFC 5322, which allows many unexpected forms of - * email addresses and will easily match a typographical error. - * - * @param addr The input string to validate as an email address. - * @return {@code true} if the input string is a valid email address, {@code false} otherwise. - */ - static boolean isEmail(String addr) { - return EMAIL_REGEX.matcher(addr).matches(); - } - - /** - * Returns true if the string is a valid hostname, for example "foo.example.com". - * - *

A valid hostname follows the rules below: - * - *

    - *
  • The name consists of one or more labels, separated by a dot ("."). - *
  • Each label can be 1 to 63 alphanumeric characters. - *
  • A label can contain hyphens ("-"), but must not start or end with a hyphen. - *
  • The right-most label must not be digits only. - *
  • The name can have a trailing dot, for example "foo.example.com.". - *
  • The name can be 253 characters at most, excluding the optional trailing dot. - *
- */ - static boolean isHostname(String val) { - if (val.length() > 253) { - return false; - } - - String str; - if (val.endsWith(".")) { - str = val.substring(0, val.length() - 1); - } else { - str = val; - } - - boolean allDigits = false; - - String[] parts = str.split("\\.", -1); - - // split hostname on '.' and validate each part - for (String part : parts) { - allDigits = true; - - // if part is empty, longer than 63 chars, or starts/ends with '-', it is - // invalid - int len = part.length(); - if (len == 0 || len > 63 || part.startsWith("-") || part.endsWith("-")) { - return false; - } - - // for each character in part - for (int i = 0; i < part.length(); i++) { - char c = part.charAt(i); - // if the character is not a-z, A-Z, 0-9, or '-', it is invalid - if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '-') { - return false; - } - - allDigits = allDigits && c >= '0' && c <= '9'; - } - } - - // the last part cannot be all numbers - return !allDigits; - } - - /** - * Returns true if the string is an IPv4 or IPv6 address, optionally limited to a specific - * version. - * - *

Version 0 means either 4 or 6. Passing a version other than 0, 4, or 6 always returns false. - * - *

IPv4 addresses are expected in the dotted decimal format, for example "192.168.5.21". IPv6 - * addresses are expected in their text representation, for example "::1", or - * "2001:0DB8:ABCD:0012::0". - * - *

Both formats are well-defined in the internet standard RFC 3986. Zone identifiers for IPv6 - * addresses (for example "fe80::a%en1") are supported. - */ - static boolean isIp(String addr, long ver) { - if (ver == 6L) { - return new Ipv6(addr).address(); - } else if (ver == 4L) { - return new Ipv4(addr).address(); - } else if (ver == 0L) { - return new Ipv4(addr).address() || new Ipv6(addr).address(); - } - return false; - } - - /** - * Returns true if the string is a URI, for example {@code - * https://example.com/foo/bar?baz=quux#frag}. - * - *

URI is defined in the internet standard RFC 3986. Zone Identifiers in IPv6 address literals - * are supported (RFC 6874). - */ - static boolean isUri(String str) { - return new Uri(str).uri(); - } - - /** - * Returns true if the string is a URI Reference - a URI such as {@code - * https://example.com/foo/bar?baz=quux#frag}, or a Relative Reference such as {@code - * ./foo/bar?query}. - * - *

URI, URI Reference, and Relative Reference are defined in the internet standard RFC 3986. - * Zone Identifiers in IPv6 address literals are supported (RFC 6874). - */ - static boolean isUriRef(String str) { - return new Uri(str).uriReference(); - } - - /** - * Returns true if the string is a valid IP with prefix length, optionally limited to a specific - * version (v4 or v6), and optionally requiring the host portion to be all zeros. - * - *

An address prefix divides an IP address into a network portion, and a host portion. The - * prefix length specifies how many bits the network portion has. For example, the IPv6 prefix - * "2001:db8:abcd:0012::0/64" designates the left-most 64 bits as the network prefix. The range of - * the network is 2**64 addresses, from 2001:db8:abcd:0012::0 to - * 2001:db8:abcd:0012:ffff:ffff:ffff:ffff. - * - *

An address prefix may include a specific host address, for example - * "2001:db8:abcd:0012::1f/64". With strict = true, this is not permitted. The host portion must - * be all zeros, as in "2001:db8:abcd:0012::0/64". - * - *

The same principle applies to IPv4 addresses. "192.168.1.0/24" designates the first 24 bits - * of the 32-bit IPv4 as the network prefix. - */ - static boolean isIpPrefix(String str, long version, boolean strict) { - if (version == 6L) { - Ipv6 ip = new Ipv6(str); - return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); - } else if (version == 4L) { - Ipv4 ip = new Ipv4(str); - return ip.addressPrefix() && (!strict || ip.isPrefixOnly()); - } else if (version == 0L) { - return isIpPrefix(str, 6, strict) || isIpPrefix(str, 4, strict); - } - return false; - } -} diff --git a/src/main/java/build/buf/protovalidate/DescriptorMappings.java b/src/main/java/build/buf/protovalidate/DescriptorMappings.java deleted file mode 100644 index dcb409f11..000000000 --- a/src/main/java/build/buf/protovalidate/DescriptorMappings.java +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Descriptors.OneofDescriptor; -import dev.cel.common.types.CelType; -import dev.cel.common.types.CelTypes; -import dev.cel.common.types.ListType; -import dev.cel.common.types.MapType; -import dev.cel.common.types.SimpleType; -import dev.cel.common.types.StructTypeReference; -import dev.cel.common.types.UnspecifiedType; -import java.util.HashMap; -import java.util.Map; -import org.jspecify.annotations.Nullable; - -/** - * DescriptorMappings provides mappings between protocol buffer descriptors and CEL declarations. - */ -final class DescriptorMappings { - /** Provides a {@link Descriptor} for {@link FieldRules}. */ - static final Descriptor FIELD_RULES_DESC = FieldRules.getDescriptor(); - - /** Provides the {@link OneofDescriptor} for the type union in {@link FieldRules}. */ - static final OneofDescriptor FIELD_RULES_ONEOF_DESC = FIELD_RULES_DESC.getOneofs().get(0); - - /** Provides the {@link FieldDescriptor} for the map standard rules. */ - static final FieldDescriptor MAP_FIELD_RULES_DESC = FIELD_RULES_DESC.findFieldByName("map"); - - /** Provides the {@link FieldDescriptor} for the repeated standard rules. */ - static final FieldDescriptor REPEATED_FIELD_RULES_DESC = - FIELD_RULES_DESC.findFieldByName("repeated"); - - /** Maps protocol buffer field kinds to their expected field rules. */ - static final Map EXPECTED_STANDARD_RULES = new HashMap<>(); - - /** - * Returns the {@link build.buf.validate.FieldRules} field that is expected for the given wrapper - * well-known type's full name. If ok is false, no standard rules exist for that type. - */ - static final Map EXPECTED_WKT_RULES = new HashMap<>(); - - static { - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.FLOAT, FIELD_RULES_DESC.findFieldByName("float")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.DOUBLE, FIELD_RULES_DESC.findFieldByName("double")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.INT32, FIELD_RULES_DESC.findFieldByName("int32")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.INT64, FIELD_RULES_DESC.findFieldByName("int64")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.UINT32, FIELD_RULES_DESC.findFieldByName("uint32")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.UINT64, FIELD_RULES_DESC.findFieldByName("uint64")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.SINT32, FIELD_RULES_DESC.findFieldByName("sint32")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.SINT64, FIELD_RULES_DESC.findFieldByName("sint64")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.FIXED32, FIELD_RULES_DESC.findFieldByName("fixed32")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.FIXED64, FIELD_RULES_DESC.findFieldByName("fixed64")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.SFIXED32, FIELD_RULES_DESC.findFieldByName("sfixed32")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.SFIXED64, FIELD_RULES_DESC.findFieldByName("sfixed64")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.BOOL, FIELD_RULES_DESC.findFieldByName("bool")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.STRING, FIELD_RULES_DESC.findFieldByName("string")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.BYTES, FIELD_RULES_DESC.findFieldByName("bytes")); - EXPECTED_STANDARD_RULES.put( - FieldDescriptor.Type.ENUM, FIELD_RULES_DESC.findFieldByName("enum")); - - EXPECTED_WKT_RULES.put("google.protobuf.Any", FIELD_RULES_DESC.findFieldByName("any")); - EXPECTED_WKT_RULES.put( - "google.protobuf.Duration", FIELD_RULES_DESC.findFieldByName("duration")); - EXPECTED_WKT_RULES.put( - "google.protobuf.FieldMask", FIELD_RULES_DESC.findFieldByName("field_mask")); - EXPECTED_WKT_RULES.put( - "google.protobuf.Timestamp", FIELD_RULES_DESC.findFieldByName("timestamp")); - } - - private DescriptorMappings() {} - - /** - * Returns the {@link FieldRules} field that is expected for the given protocol buffer field kind. - * - * @param fqn Fully qualified name of protobuf value wrapper. - * @return The rules field descriptor for the specified wrapper fully qualified name. - */ - @Nullable - static FieldDescriptor expectedWrapperRules(String fqn) { - switch (fqn) { - case "google.protobuf.BoolValue": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.BOOL); - case "google.protobuf.BytesValue": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.BYTES); - case "google.protobuf.DoubleValue": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.DOUBLE); - case "google.protobuf.FloatValue": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.FLOAT); - case "google.protobuf.Int32Value": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.INT32); - case "google.protobuf.Int64Value": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.INT64); - case "google.protobuf.StringValue": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.STRING); - case "google.protobuf.UInt32Value": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.UINT32); - case "google.protobuf.UInt64Value": - return EXPECTED_STANDARD_RULES.get(FieldDescriptor.Type.UINT64); - default: - return null; - } - } - - /** - * Maps a {@link FieldDescriptor.Type} to a compatible {@link dev.cel.common.types.CelType}. - * - * @param kind The protobuf field type. - * @return The corresponding CEL type for the protobuf field. - */ - static CelType protoKindToCELType(FieldDescriptor.Type kind) { - switch (kind) { - case FLOAT: - case DOUBLE: - return SimpleType.DOUBLE; - case INT32: - case INT64: - case SINT32: - case SINT64: - case SFIXED32: - case SFIXED64: - case ENUM: - return SimpleType.INT; - case UINT32: - case UINT64: - case FIXED32: - case FIXED64: - return SimpleType.UINT; - case BOOL: - return SimpleType.BOOL; - case STRING: - return SimpleType.STRING; - case BYTES: - return SimpleType.BYTES; - case MESSAGE: - case GROUP: - return StructTypeReference.create(kind.getJavaType().name()); - default: - return UnspecifiedType.create(); - } - } - - /** - * Produces the field descriptor from the {@link FieldRules} 'type' oneof that matches the - * provided target field descriptor. If the returned value is null, the field does not expect any - * standard rules. - */ - @Nullable - static FieldDescriptor getExpectedRuleDescriptor( - FieldDescriptor fieldDescriptor, boolean forItems) { - if (fieldDescriptor.isMapField()) { - return DescriptorMappings.MAP_FIELD_RULES_DESC; - } else if (fieldDescriptor.isRepeated() && !forItems) { - return DescriptorMappings.REPEATED_FIELD_RULES_DESC; - } else if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - return DescriptorMappings.EXPECTED_WKT_RULES.get( - fieldDescriptor.getMessageType().getFullName()); - } else { - return DescriptorMappings.EXPECTED_STANDARD_RULES.get(fieldDescriptor.getType()); - } - } - - /** - * Resolves the CEL value type for the provided {@link FieldDescriptor}. If forItems is true, the - * type for the repeated list items is returned instead of the list type itself. - */ - static CelType getCELType(FieldDescriptor fieldDescriptor, boolean forItems) { - if (!forItems) { - if (fieldDescriptor.isMapField()) { - return MapType.create( - getCELType(fieldDescriptor.getMessageType().findFieldByNumber(1), true), - getCELType(fieldDescriptor.getMessageType().findFieldByNumber(2), true)); - } else if (fieldDescriptor.isRepeated()) { - return ListType.create(getCELType(fieldDescriptor, true)); - } - } - - if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - String fqn = fieldDescriptor.getMessageType().getFullName(); - return CelTypes.getWellKnownCelType(fqn).orElse(StructTypeReference.create(fqn)); - } - return DescriptorMappings.protoKindToCELType(fieldDescriptor.getType()); - } -} diff --git a/src/main/java/build/buf/protovalidate/EmbeddedMessageEvaluator.java b/src/main/java/build/buf/protovalidate/EmbeddedMessageEvaluator.java deleted file mode 100644 index 1b931fc40..000000000 --- a/src/main/java/build/buf/protovalidate/EmbeddedMessageEvaluator.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import java.util.Collections; -import java.util.List; - -final class EmbeddedMessageEvaluator implements Evaluator { - private final RuleViolationHelper helper; - private final MessageEvaluator messageEvaluator; - - EmbeddedMessageEvaluator(ValueEvaluator valueEvaluator, MessageEvaluator messageEvaluator) { - this.helper = new RuleViolationHelper(valueEvaluator); - this.messageEvaluator = messageEvaluator; - } - - @Override - public boolean tautology() { - return messageEvaluator.tautology(); - } - - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - return FieldPathUtils.updatePaths( - messageEvaluator.evaluate(val, failFast), - helper.getFieldPathElement(), - Collections.emptyList()); - } -} diff --git a/src/main/java/build/buf/protovalidate/EnumEvaluator.java b/src/main/java/build/buf/protovalidate/EnumEvaluator.java deleted file mode 100644 index d2fa00df4..000000000 --- a/src/main/java/build/buf/protovalidate/EnumEvaluator.java +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.EnumRules; -import build.buf.validate.FieldPath; -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * {@link EnumEvaluator} checks an enum value being a member of the defined values exclusively. This - * check is handled outside CEL as enums are completely type erased to integers. - */ -final class EnumEvaluator implements Evaluator { - private final RuleViolationHelper helper; - - /** Captures all the defined values for this enum */ - private final Set values; - - private static final Descriptors.FieldDescriptor DEFINED_ONLY_DESCRIPTOR = - EnumRules.getDescriptor().findFieldByNumber(EnumRules.DEFINED_ONLY_FIELD_NUMBER); - - private static final FieldPath DEFINED_ONLY_RULE_PATH = - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor().findFieldByNumber(FieldRules.ENUM_FIELD_NUMBER))) - .addElements(FieldPathUtils.fieldPathElement(DEFINED_ONLY_DESCRIPTOR)) - .build(); - - /** - * Constructs a new evaluator for enum values. - * - * @param valueDescriptors the list of {@link Descriptors.EnumValueDescriptor} for the enum. - */ - EnumEvaluator( - ValueEvaluator valueEvaluator, List valueDescriptors) { - this.helper = new RuleViolationHelper(valueEvaluator); - if (valueDescriptors.isEmpty()) { - this.values = Collections.emptySet(); - } else { - this.values = - valueDescriptors.stream().map(it -> (long) it.getNumber()).collect(Collectors.toSet()); - } - } - - @Override - public boolean tautology() { - return false; - } - - /** - * Evaluates an enum value. - * - * @param val the value to evaluate. - * @param failFast indicates if the evaluation should stop on the first violation. - * @return the {@link ValidationResult} of the evaluation. - * @throws ExecutionException if an error occurs during the evaluation. - */ - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - Object enumValue = val.value(Object.class); - if (enumValue == null) { - return RuleViolation.NO_VIOLATIONS; - } - if (!values.contains(enumValue)) { - return Collections.singletonList( - RuleViolation.newBuilder() - .addAllRulePathElements(helper.getRulePrefixElements()) - .addAllRulePathElements(DEFINED_ONLY_RULE_PATH.getElementsList()) - .addFirstFieldPathElement(helper.getFieldPathElement()) - .setRuleId("enum.defined_only") - .setMessage("value must be one of the defined enum values") - .setFieldValue(new RuleViolation.FieldValue(val)) - .setRuleValue(new RuleViolation.FieldValue(true, DEFINED_ONLY_DESCRIPTOR))); - } - return RuleViolation.NO_VIOLATIONS; - } -} diff --git a/src/main/java/build/buf/protovalidate/EnumRulesEvaluator.java b/src/main/java/build/buf/protovalidate/EnumRulesEvaluator.java deleted file mode 100644 index 99ffc4367..000000000 --- a/src/main/java/build/buf/protovalidate/EnumRulesEvaluator.java +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.EnumRules; -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors.EnumValueDescriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.List; -import org.jspecify.annotations.Nullable; - -/** - * Native evaluator for enum {@code const}/{@code in}/{@code not_in}. The {@code defined_only} rule - * is handled separately by the existing {@link build.buf.protovalidate.EnumEvaluator}; both can be - * active simultaneously and the {@link build.buf.protovalidate.ValueEvaluator} runs them in order. - */ -final class EnumRulesEvaluator implements Evaluator { - private static final FieldDescriptor ENUM_RULES_DESC = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.ENUM_FIELD_NUMBER); - private static final FieldDescriptor CONST_DESC = - EnumRules.getDescriptor().findFieldByNumber(EnumRules.CONST_FIELD_NUMBER); - private static final FieldDescriptor IN_DESC = - EnumRules.getDescriptor().findFieldByNumber(EnumRules.IN_FIELD_NUMBER); - private static final FieldDescriptor NOT_IN_DESC = - EnumRules.getDescriptor().findFieldByNumber(EnumRules.NOT_IN_FIELD_NUMBER); - private static final RuleSite CONST_SITE = - RuleSite.of(ENUM_RULES_DESC, CONST_DESC, "enum.const", null); - private static final RuleSite IN_SITE = RuleSite.of(ENUM_RULES_DESC, IN_DESC, "enum.in", null); - private static final RuleSite NOT_IN_SITE = - RuleSite.of(ENUM_RULES_DESC, NOT_IN_DESC, "enum.not_in", null); - - private final RuleBase base; - private final @Nullable Integer constVal; - private final List inVals; - private final List notInVals; - - private EnumRulesEvaluator( - RuleBase base, @Nullable Integer constVal, List inVals, List notInVals) { - this.base = base; - this.constVal = constVal; - this.inVals = inVals; - this.notInVals = notInVals; - } - - /** - * Builds a {@link EnumRulesEvaluator} for the {@code const}/{@code in}/{@code not_in} rules on - * the supplied {@code FieldRules.Builder}'s enum sub-message. Returns null when the enum sub- - * message is unset, has unknown fields, or has none of the covered rules. The {@code - * defined_only} field is left untouched on the residual so the existing {@link - * build.buf.protovalidate.EnumEvaluator} continues to handle it. - */ - static @Nullable Evaluator tryBuild(RuleBase base, FieldRules.Builder rulesBuilder) { - if (!rulesBuilder.hasEnum()) { - return null; - } - EnumRules enumRules = rulesBuilder.getEnum(); - if (!enumRules.getUnknownFields().isEmpty()) { - return null; - } - - EnumRules.Builder eb = enumRules.toBuilder(); - boolean hasRule = false; - - Integer constVal = null; - if (enumRules.hasConst()) { - constVal = enumRules.getConst(); - eb.clearConst(); - hasRule = true; - } - - // Proto returns immutable views; we only read them. - List inVals = enumRules.getInList(); - if (!inVals.isEmpty()) { - eb.clearIn(); - hasRule = true; - } - - List notInVals = enumRules.getNotInList(); - if (!notInVals.isEmpty()) { - eb.clearNotIn(); - hasRule = true; - } - - if (!hasRule) { - return null; - } - rulesBuilder.setEnum(eb.build()); - return new EnumRulesEvaluator(base, constVal, inVals, notInVals); - } - - @Override - public boolean tautology() { - return false; - } - - @Override - public List evaluate(Value val, boolean failFast) { - int actual = enumNumber(val.rawValue()); - List violations = null; - - if (constVal != null && actual != constVal) { - RuleViolation.Builder b = - NativeViolations.newViolation(CONST_SITE, null, "must equal " + constVal, val, constVal); - violations = RuleBase.add(violations, b); - if (failFast) { - return base.done(violations); - } - } - - if (!inVals.isEmpty() && !inVals.contains(actual)) { - RuleViolation.Builder b = - NativeViolations.newViolation( - IN_SITE, null, "must be in list " + RuleBase.formatList(inVals), val, inVals); - violations = RuleBase.add(violations, b); - if (failFast) { - return base.done(violations); - } - } - - if (!notInVals.isEmpty() && notInVals.contains(actual)) { - RuleViolation.Builder b = - NativeViolations.newViolation( - NOT_IN_SITE, - null, - "must not be in list " + RuleBase.formatList(notInVals), - val, - notInVals); - violations = RuleBase.add(violations, b); - if (failFast) { - return base.done(violations); - } - } - - return base.done(violations); - } - - /** - * Extracts the enum's numeric value from {@link Value#rawValue()}. Java protobuf normally returns - * an {@link EnumValueDescriptor}, but unknown enum values may surface as {@link Integer} - * depending on the proto edition; handle both. - */ - private static int enumNumber(Object raw) { - if (raw instanceof EnumValueDescriptor) { - return ((EnumValueDescriptor) raw).getNumber(); - } - if (raw instanceof Integer) { - return (Integer) raw; - } - // the enum wire format says that enums are encoded as though they are int32s. - // https://protobuf.dev/programming-guides/encoding/ I don't know if the value could - // end up in a Long somehow, so coding defensively around that. If a value out of - // 32-bit int range shows up, Math.toIntExact will throw an exception. - if (raw instanceof Long) { - return Math.toIntExact((Long) raw); - } - throw new IllegalStateException( - "unexpected enum value representation: " + raw.getClass().getName()); - } -} diff --git a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java deleted file mode 100644 index 00348f4cb..000000000 --- a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java +++ /dev/null @@ -1,590 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.CompilationException; -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import build.buf.validate.FieldRules; -import build.buf.validate.Ignore; -import build.buf.validate.MessageOneofRule; -import build.buf.validate.MessageRules; -import build.buf.validate.OneofRules; -import build.buf.validate.Rule; -import com.google.protobuf.Descriptors; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; -import dev.cel.bundle.Cel; -import dev.cel.bundle.CelBuilder; -import dev.cel.common.types.StructTypeReference; -import dev.cel.common.values.CelByteString; -import dev.cel.runtime.CelEvaluationException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.jspecify.annotations.Nullable; - -/** A build-through cache of message evaluators keyed off the provided descriptor. */ -final class EvaluatorBuilder { - private static final FieldPathElement CEL_FIELD_PATH_ELEMENT = - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor().findFieldByNumber(FieldRules.CEL_FIELD_NUMBER)); - - private static final FieldPathElement CEL_EXPRESSION_FIELD_PATH_ELEMENT = - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor().findFieldByNumber(FieldRules.CEL_EXPRESSION_FIELD_NUMBER)); - - private volatile Map evaluatorCache = Collections.emptyMap(); - - private final Cel cel; - private final boolean disableLazy; - private final boolean enableNativeRules; - private final RuleCache rules; - - /** - * Constructs a new {@link EvaluatorBuilder}. - * - * @param cel The CEL environment for evaluation. - * @param config The configuration to use for the evaluation. - */ - EvaluatorBuilder(Cel cel, Config config) { - this(cel, config, false); - } - - private EvaluatorBuilder(Cel cel, Config config, boolean disableLazy) { - this.cel = cel; - this.disableLazy = disableLazy; - this.enableNativeRules = config.isNativeRulesEnabled(); - this.rules = new RuleCache(cel, config); - } - - /** - * Constructs a new {@link EvaluatorBuilder}. - * - * @param cel The CEL environment for evaluation. - * @param config The configuration to use for the evaluation. - * @param descriptors The descriptors to build evaluators for. Must be non-null. - * @param disableLazy If true, the builder will not cache evaluators for descriptors that are not - * @throws CompilationException If an evaluator can't be built for a descriptor. - */ - EvaluatorBuilder(Cel cel, Config config, List descriptors, boolean disableLazy) - throws CompilationException { - this(cel, config, disableLazy); - Objects.requireNonNull(descriptors, "descriptors must not be null"); - for (Descriptor descriptor : descriptors) { - this.build(descriptor); - } - } - - /** - * Returns a pre-cached {@link Evaluator} for the given descriptor or, if the descriptor is - * unknown, returns an evaluator that always throws a {@link CompilationException}. - * - * @param desc Protobuf descriptor type. - * @return An evaluator for the descriptor type. - * @throws CompilationException If an evaluator can't be created for the specified descriptor. - */ - Evaluator load(Descriptor desc) throws CompilationException { - Evaluator evaluator = evaluatorCache.get(desc); - if (evaluator == null && disableLazy) { - return new UnknownDescriptorEvaluator(desc); - } - return build(desc); - } - - /** - * Either returns a memoized {@link Evaluator} for the given descriptor, or lazily constructs a - * new one. - */ - private Evaluator build(Descriptor desc) throws CompilationException { - Evaluator eval = evaluatorCache.get(desc); - if (eval != null) { - return eval; - } - synchronized (this) { - // Check again (we may have lost race with another thread which populated the map with this - // descriptor). - eval = evaluatorCache.get(desc); - if (eval != null) { - return eval; - } - // Rebuild cache with this descriptor (and any of its dependencies). - Map updatedCache = - new DescriptorCacheBuilder(cel, rules, enableNativeRules, evaluatorCache).build(desc); - evaluatorCache = updatedCache; - eval = updatedCache.get(desc); - if (eval == null) { - throw new IllegalStateException( - "updated cache missing evaluator for descriptor - should not happen"); - } - } - return eval; - } - - private static class DescriptorCacheBuilder { - private final RuleResolver resolver = new RuleResolver(); - private final Cel cel; - private final RuleCache ruleCache; - private final boolean enableNativeRules; - private final HashMap cache; - - private DescriptorCacheBuilder( - Cel cel, - RuleCache ruleCache, - boolean enableNativeRules, - Map previousCache) { - this.cel = Objects.requireNonNull(cel, "cel"); - this.ruleCache = Objects.requireNonNull(ruleCache, "ruleCache"); - this.enableNativeRules = enableNativeRules; - this.cache = new HashMap<>(previousCache); - } - - /** - * Creates an immutable cache containing the descriptor (and any other descriptors it - * references). - * - * @param descriptor Descriptor used to build the cache. - * @return Unmodifiable map of descriptors to evaluators. - * @throws CompilationException If an error occurs compiling a rule on the cache. - */ - Map build(Descriptor descriptor) throws CompilationException { - createMessageEvaluator(descriptor); - return Collections.unmodifiableMap(cache); - } - - private MessageEvaluator createMessageEvaluator(Descriptor desc) throws CompilationException { - MessageEvaluator eval = cache.get(desc); - if (eval != null) { - return eval; - } - MessageEvaluator msgEval = new MessageEvaluator(); - cache.put(desc, msgEval); - buildMessage(desc, msgEval); - return msgEval; - } - - private void buildMessage(Descriptor desc, MessageEvaluator msgEval) - throws CompilationException { - try { - DynamicMessage defaultInstance = DynamicMessage.newBuilder(desc).buildPartial(); - Descriptor descriptor = defaultInstance.getDescriptorForType(); - MessageRules msgRules = resolver.resolveMessageRules(descriptor); - processMessageExpressions(descriptor, msgRules, msgEval, defaultInstance); - processMessageOneofRules(descriptor, msgRules, msgEval); - processOneofRules(descriptor, msgEval); - processFields(descriptor, msgRules, msgEval); - } catch (InvalidProtocolBufferException e) { - throw new CompilationException( - "failed to parse proto definition: " + desc.getFullName(), e); - } - } - - private void processMessageExpressions( - Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval, DynamicMessage message) - throws CompilationException { - List celList = - Stream.concat( - expressionsToRules(msgRules.getCelExpressionList()).stream(), - msgRules.getCelList().stream()) - .collect(Collectors.toList()); - if (celList.isEmpty()) { - return; - } - Cel finalCel = - cel.toCelBuilder() - .addMessageTypes(message.getDescriptorForType()) - .addVar(Variable.THIS_NAME, StructTypeReference.create(desc.getFullName())) - .build(); - List compiledPrograms = compileRules(celList, finalCel, null); - if (compiledPrograms.isEmpty()) { - throw new CompilationException("compile returned null"); - } - msgEval.append(new CelPrograms(null, compiledPrograms)); - } - - private void processMessageOneofRules( - Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval) - throws CompilationException { - for (MessageOneofRule rule : msgRules.getOneofList()) { - if (rule.getFieldsCount() == 0) { - throw new CompilationException( - String.format( - "at least one field must be specified in oneof rule for the message %s", - desc.getFullName())); - } - Set fields = new LinkedHashSet<>(rule.getFieldsCount()); - for (String name : rule.getFieldsList()) { - FieldDescriptor field = desc.findFieldByName(name); - if (field == null) { - throw new CompilationException( - String.format("field %s not found in %s", name, desc.getFullName())); - } - if (!fields.add(field)) { - throw new CompilationException( - String.format( - "duplicate %s in oneof rule for the message %s", name, desc.getFullName())); - } - } - msgEval.append(new MessageOneofEvaluator(new ArrayList<>(fields), rule.getRequired())); - } - } - - private void processOneofRules(Descriptor desc, MessageEvaluator msgEval) - throws InvalidProtocolBufferException, CompilationException { - List oneofs = desc.getOneofs(); - for (Descriptors.OneofDescriptor oneofDesc : oneofs) { - OneofRules oneofRules = resolver.resolveOneofRules(oneofDesc); - OneofEvaluator oneofEvaluatorEval = new OneofEvaluator(oneofDesc, oneofRules.getRequired()); - msgEval.append(oneofEvaluatorEval); - } - } - - private void processFields(Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval) - throws CompilationException, InvalidProtocolBufferException { - List fields = desc.getFields(); - for (FieldDescriptor fieldDescriptor : fields) { - FieldDescriptor descriptor = desc.findFieldByName(fieldDescriptor.getName()); - FieldRules fieldRules = resolver.resolveFieldRules(descriptor); - if (!fieldRules.hasIgnore() - && msgRules.getOneofList().stream() - .anyMatch(oneof -> oneof.getFieldsList().contains(fieldDescriptor.getName()))) { - fieldRules = fieldRules.toBuilder().setIgnore(Ignore.IGNORE_IF_ZERO_VALUE).build(); - } - FieldEvaluator fldEval = buildField(descriptor, fieldRules); - msgEval.append(fldEval); - } - } - - private FieldEvaluator buildField(FieldDescriptor fieldDescriptor, FieldRules fieldRules) - throws CompilationException { - ValueEvaluator valueEvaluatorEval = new ValueEvaluator(fieldDescriptor, null); - FieldEvaluator fieldEvaluator = - new FieldEvaluator( - valueEvaluatorEval, - fieldDescriptor, - fieldRules.getRequired(), - fieldDescriptor.hasPresence(), - fieldRules.getIgnore()); - buildValue(fieldDescriptor, fieldRules, fieldEvaluator.valueEvaluator); - return fieldEvaluator; - } - - private static boolean shouldIgnoreEmpty(FieldRules rules) { - return rules.getIgnore() == Ignore.IGNORE_IF_ZERO_VALUE; - } - - private void buildValue( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluator) - throws CompilationException { - if (fieldRules.getIgnore() == Ignore.IGNORE_ALWAYS) { - return; - } - - processIgnoreEmpty(fieldDescriptor, fieldRules, valueEvaluator); - processFieldExpressions(fieldDescriptor, fieldRules, valueEvaluator); - processEmbeddedMessage(fieldDescriptor, valueEvaluator); - processWrapperRules(fieldDescriptor, fieldRules, valueEvaluator); - processStandardRules(fieldDescriptor, fieldRules, valueEvaluator); - processAnyRules(fieldDescriptor, fieldRules, valueEvaluator); - processEnumRules(fieldDescriptor, fieldRules, valueEvaluator); - processMapRules(fieldDescriptor, fieldRules, valueEvaluator); - processRepeatedRules(fieldDescriptor, fieldRules, valueEvaluator); - } - - private void processIgnoreEmpty( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - if (valueEvaluatorEval.hasNestedRule() && shouldIgnoreEmpty(fieldRules)) { - valueEvaluatorEval.setIgnoreEmpty(zeroValue(fieldDescriptor, true)); - } - } - - private Object zeroValue(FieldDescriptor fieldDescriptor, boolean forItems) - throws CompilationException { - final Object zero; - if (forItems && fieldDescriptor.isRepeated()) { - switch (fieldDescriptor.getType().getJavaType()) { - case INT: - case LONG: - zero = 0L; - break; - case FLOAT: - case DOUBLE: - zero = 0D; - break; - case BOOLEAN: - zero = false; - break; - case STRING: - zero = ""; - break; - case BYTE_STRING: - zero = CelByteString.EMPTY; - break; - case ENUM: - zero = (long) fieldDescriptor.getEnumType().getValues().get(0).getNumber(); - break; - case MESSAGE: - zero = createMessageForType(fieldDescriptor.getMessageType()); - break; - default: - zero = fieldDescriptor.getDefaultValue(); - break; - } - } else if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE - && !fieldDescriptor.isRepeated()) { - zero = createMessageForType(fieldDescriptor.getMessageType()); - } else { - zero = - ProtoAdapter.scalarToCel(fieldDescriptor.getType(), fieldDescriptor.getDefaultValue()); - } - return zero; - } - - private Message createMessageForType(Descriptor messageType) throws CompilationException { - try { - return DynamicMessage.parseFrom(messageType, new byte[0]); - } catch (InvalidProtocolBufferException e) { - throw new CompilationException("field descriptor type is invalid " + e.getMessage(), e); - } - } - - private void processFieldExpressions( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - List rulesCelList = fieldRules.getCelList(); - List exprList = fieldRules.getCelExpressionList(); - if (rulesCelList.isEmpty() && exprList.isEmpty()) { - return; - } - CelBuilder builder = cel.toCelBuilder(); - builder = - builder.addVar( - Variable.THIS_NAME, - DescriptorMappings.getCELType(fieldDescriptor, valueEvaluatorEval.hasNestedRule())); - - if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - builder = builder.addMessageTypes(fieldDescriptor.getMessageType()); - } - Cel finalCel = builder.build(); - List compiledPrograms = new ArrayList<>(); - if (!rulesCelList.isEmpty()) { - compiledPrograms.addAll(compileRules(rulesCelList, finalCel, CEL_FIELD_PATH_ELEMENT)); - } - if (!exprList.isEmpty()) { - compiledPrograms.addAll( - compileRules( - expressionsToRules(exprList), finalCel, CEL_EXPRESSION_FIELD_PATH_ELEMENT)); - } - - if (!compiledPrograms.isEmpty()) { - valueEvaluatorEval.append(new CelPrograms(valueEvaluatorEval, compiledPrograms)); - } - } - - private void processEmbeddedMessage( - FieldDescriptor fieldDescriptor, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE - || fieldDescriptor.isMapField() - || (fieldDescriptor.isRepeated() && !valueEvaluatorEval.hasNestedRule())) { - return; - } - Evaluator embedEval = - new EmbeddedMessageEvaluator( - valueEvaluatorEval, createMessageEvaluator(fieldDescriptor.getMessageType())); - valueEvaluatorEval.append(embedEval); - } - - private void processWrapperRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - - if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE - || fieldDescriptor.isMapField() - || (fieldDescriptor.isRepeated() && !valueEvaluatorEval.hasNestedRule())) { - return; - } - FieldDescriptor expectedWrapperDescriptor = - DescriptorMappings.expectedWrapperRules(fieldDescriptor.getMessageType().getFullName()); - - // Verify that the expected wrapper rules for this field are equal to the rules specified on - // the field - if (expectedWrapperDescriptor != null) { - FieldDescriptor oneofFieldDescriptor = - fieldRules.getOneofFieldDescriptor(DescriptorMappings.FIELD_RULES_ONEOF_DESC); - // If there are no field rules set, just return - if (oneofFieldDescriptor == null) { - return; - } - if (!expectedWrapperDescriptor - .getMessageType() - .getFullName() - .equals(oneofFieldDescriptor.getMessageType().getFullName())) { - throw new CompilationException( - String.format( - "mismatched message rules, %s is not a valid rule for field %s", - oneofFieldDescriptor.getName(), fieldDescriptor.getName())); - } - } - if (expectedWrapperDescriptor == null || !fieldRules.hasField(expectedWrapperDescriptor)) { - return; - } - - ValueEvaluator unwrapped = - new ValueEvaluator( - valueEvaluatorEval.getDescriptor(), valueEvaluatorEval.getNestedRule()); - buildValue(fieldDescriptor.getMessageType().findFieldByName("value"), fieldRules, unwrapped); - valueEvaluatorEval.append(unwrapped); - } - - private void processStandardRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - - // If this is a wrapper field, just return. Wrapper fields are handled by - // processWrapperRules and their unwrapped values are passed through the process gauntlet. - if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - FieldDescriptor expectedWrapperDescriptor = - DescriptorMappings.expectedWrapperRules(fieldDescriptor.getMessageType().getFullName()); - if (expectedWrapperDescriptor != null) { - return; - } - } - - // Try native rule evaluators when opted in. Any rule covered natively is cleared on the - // residual builder so CEL only compiles what's left; rules without a native implementation - // remain on the residual and CEL handles them. - if (enableNativeRules) { - FieldRules.Builder rulesBuilder = fieldRules.toBuilder(); - Evaluator nativeEval = Rules.tryBuild(fieldDescriptor, rulesBuilder, valueEvaluatorEval); - if (nativeEval != null) { - valueEvaluatorEval.append(nativeEval); - fieldRules = rulesBuilder.build(); - } - } - - List compile = - ruleCache.compile(fieldDescriptor, fieldRules, valueEvaluatorEval.hasNestedRule()); - if (compile.isEmpty()) { - return; - } - valueEvaluatorEval.append(new CelPrograms(valueEvaluatorEval, compile)); - } - - private void processAnyRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) { - if ((fieldDescriptor.isRepeated() && !valueEvaluatorEval.hasNestedRule()) - || fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE - || !fieldDescriptor.getMessageType().getFullName().equals("google.protobuf.Any")) { - return; - } - FieldDescriptor typeURLDesc = fieldDescriptor.getMessageType().findFieldByName("type_url"); - valueEvaluatorEval.append( - new AnyEvaluator( - valueEvaluatorEval, - typeURLDesc, - fieldRules.getAny().getInList(), - fieldRules.getAny().getNotInList())); - } - - private void processEnumRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) { - if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.ENUM) { - return; - } - if (fieldRules.getEnum().getDefinedOnly()) { - Descriptors.EnumDescriptor enumDescriptor = fieldDescriptor.getEnumType(); - valueEvaluatorEval.append( - new EnumEvaluator(valueEvaluatorEval, enumDescriptor.getValues())); - } - } - - private void processMapRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - if (!fieldDescriptor.isMapField()) { - return; - } - MapEvaluator mapEval = new MapEvaluator(valueEvaluatorEval, fieldDescriptor); - buildValue( - fieldDescriptor.getMessageType().findFieldByNumber(1), - fieldRules.getMap().getKeys(), - mapEval.getKeyEvaluator()); - buildValue( - fieldDescriptor.getMessageType().findFieldByNumber(2), - fieldRules.getMap().getValues(), - mapEval.getValueEvaluator()); - valueEvaluatorEval.append(mapEval); - } - - private void processRepeatedRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval) - throws CompilationException { - if (fieldDescriptor.isMapField() - || !fieldDescriptor.isRepeated() - || valueEvaluatorEval.hasNestedRule()) { - return; - } - ListEvaluator listEval = new ListEvaluator(valueEvaluatorEval); - buildValue(fieldDescriptor, fieldRules.getRepeated().getItems(), listEval.itemRules); - valueEvaluatorEval.append(listEval); - } - - private static List compileRules( - List rules, Cel cel, @Nullable FieldPathElement fieldPathElement) - throws CompilationException { - List expressions = Expression.fromRules(rules); - List compiledPrograms = new ArrayList<>(); - for (int i = 0; i < expressions.size(); i++) { - Expression expression = expressions.get(i); - AstExpression astExpression = AstExpression.newAstExpression(cel, expression); - @Nullable FieldPath rulePath = null; - if (fieldPathElement != null) { - rulePath = - FieldPath.newBuilder().addElements(fieldPathElement.toBuilder().setIndex(i)).build(); - } - try { - compiledPrograms.add( - new CompiledProgram( - cel.createProgram(astExpression.ast), - astExpression.source, - rulePath, - new MessageValue(rules.get(i)), - null)); - } catch (CelEvaluationException e) { - throw new CompilationException("failed to evaluate rule " + rules.get(i).getId(), e); - } - } - return compiledPrograms; - } - - private static List expressionsToRules(List expressions) { - return expressions.stream() - .map(expr -> Rule.newBuilder().setId(expr).setExpression(expr).build()) - .collect(Collectors.toList()); - } - } -} diff --git a/src/main/java/build/buf/protovalidate/Expression.java b/src/main/java/build/buf/protovalidate/Expression.java deleted file mode 100644 index 4d3978c44..000000000 --- a/src/main/java/build/buf/protovalidate/Expression.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.Rule; -import java.util.ArrayList; -import java.util.List; - -/** Expression represents a single CEL expression. */ -final class Expression { - /** The id of the rule. */ - final String id; - - /** The message of the rule. */ - final String message; - - /** The expression of the rule. */ - final String expression; - - /** - * Constructs a new Expression. - * - * @param id The ID of the rule. - * @param message The message of the rule. - * @param expression The expression of the rule. - */ - private Expression(String id, String message, String expression) { - this.id = id; - this.message = message; - this.expression = expression; - } - - /** - * Constructs a new Expression from the given rule. - * - * @param rule The rule to create the expression from. - */ - private Expression(Rule rule) { - this(rule.getId(), rule.getMessage(), rule.getExpression()); - } - - /** - * Constructs a new list of {@link Expression} from the given list of rules. - * - * @param rules The list of rules. - * @return The list of expressions. - */ - static List fromRules(List rules) { - List expressions = new ArrayList<>(); - for (build.buf.validate.Rule rule : rules) { - expressions.add(new Expression(rule)); - } - return expressions; - } -} diff --git a/src/main/java/build/buf/protovalidate/FieldEvaluator.java b/src/main/java/build/buf/protovalidate/FieldEvaluator.java deleted file mode 100644 index 877aec5ea..000000000 --- a/src/main/java/build/buf/protovalidate/FieldEvaluator.java +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.FieldPath; -import build.buf.validate.FieldRules; -import build.buf.validate.Ignore; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; -import java.util.Collections; -import java.util.List; - -/** Performs validation on a single message field, defined by its descriptor. */ -final class FieldEvaluator implements Evaluator { - private static final FieldDescriptor REQUIRED_DESCRIPTOR = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.REQUIRED_FIELD_NUMBER); - - private static final FieldPath REQUIRED_RULE_PATH = - FieldPath.newBuilder() - .addElements(FieldPathUtils.fieldPathElement(REQUIRED_DESCRIPTOR)) - .build(); - - private final RuleViolationHelper helper; - - /** The {@link ValueEvaluator} to apply to the field's value */ - final ValueEvaluator valueEvaluator; - - /** The {@link FieldDescriptor} targeted by this evaluator */ - private final FieldDescriptor descriptor; - - /** Indicates that the field must have a set value. */ - private final boolean required; - - /** Whether validation should be ignored for certain conditions */ - private final Ignore ignore; - - /** Whether the field distinguishes between unpopulated and default values. */ - private final boolean hasPresence; - - /** Constructs a new {@link FieldEvaluator} */ - FieldEvaluator( - ValueEvaluator valueEvaluator, - FieldDescriptor descriptor, - boolean required, - boolean hasPresence, - Ignore ignore) { - this.helper = new RuleViolationHelper(valueEvaluator); - this.valueEvaluator = valueEvaluator; - this.descriptor = descriptor; - this.required = required; - this.hasPresence = hasPresence; - this.ignore = ignore; - } - - @Override - public boolean tautology() { - return !required && valueEvaluator.tautology(); - } - - /** - * Returns whether a field should always skip validation. - * - *

If true, this will take precedence and all checks are skipped. - */ - private boolean shouldIgnoreAlways() { - return this.ignore == Ignore.IGNORE_ALWAYS; - } - - /** - * Returns whether a field should skip validation on its zero value. - * - *

This is generally true for nullable fields or fields with the ignore_empty rule explicitly - * set. - */ - private boolean shouldIgnoreEmpty() { - return this.hasPresence || this.ignore == Ignore.IGNORE_IF_ZERO_VALUE; - } - - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - if (this.shouldIgnoreAlways()) { - return RuleViolation.NO_VIOLATIONS; - } - Message message = val.messageValue(); - if (message == null) { - return RuleViolation.NO_VIOLATIONS; - } - boolean hasField = isFieldSet(message, descriptor); - if (required && !hasField) { - return Collections.singletonList( - RuleViolation.newBuilder() - .addFirstFieldPathElement(FieldPathUtils.fieldPathElement(descriptor)) - .addAllRulePathElements(helper.getRulePrefixElements()) - .addAllRulePathElements(REQUIRED_RULE_PATH.getElementsList()) - .setRuleId("required") - .setMessage("value is required") - .setRuleValue(new RuleViolation.FieldValue(true, REQUIRED_DESCRIPTOR))); - } - if (this.shouldIgnoreEmpty() && !hasField) { - return RuleViolation.NO_VIOLATIONS; - } - return valueEvaluator.evaluate( - new ObjectValue(descriptor, message.getField(descriptor)), failFast); - } - - /** - * Returns whether the given field is set on the message. Handles repeated and map fields, which - * are not supported by {@link Message#hasField}. - */ - static boolean isFieldSet(Message message, FieldDescriptor field) { - if (field.isRepeated()) { - return message.getRepeatedFieldCount(field) != 0; - } - return message.hasField(field); - } -} diff --git a/src/main/java/build/buf/protovalidate/FieldPathUtils.java b/src/main/java/build/buf/protovalidate/FieldPathUtils.java deleted file mode 100644 index 4d34cfd38..000000000 --- a/src/main/java/build/buf/protovalidate/FieldPathUtils.java +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import com.google.protobuf.Descriptors; -import java.util.List; -import org.jspecify.annotations.Nullable; - -/** Utility class for manipulating error paths in violations. */ -final class FieldPathUtils { - private FieldPathUtils() {} - - /** - * Converts the provided field path to a string. - * - * @param fieldPath A field path to convert to a string. - * @return The string representation of the provided field path. - */ - static String fieldPathString(FieldPath fieldPath) { - StringBuilder builder = new StringBuilder(); - for (FieldPathElement element : fieldPath.getElementsList()) { - if (builder.length() > 0) { - builder.append("."); - } - builder.append(element.getFieldName()); - switch (element.getSubscriptCase()) { - case INDEX: - builder.append("["); - builder.append(element.getIndex()); - builder.append("]"); - break; - case BOOL_KEY: - if (element.getBoolKey()) { - builder.append("[true]"); - } else { - builder.append("[false]"); - } - break; - case INT_KEY: - builder.append("["); - builder.append(element.getIntKey()); - builder.append("]"); - break; - case UINT_KEY: - builder.append("["); - builder.append(element.getUintKey()); - builder.append("]"); - break; - case STRING_KEY: - builder.append("[\""); - builder.append(element.getStringKey().replace("\\", "\\\\").replace("\"", "\\\"")); - builder.append("\"]"); - break; - case SUBSCRIPT_NOT_SET: - break; - } - } - return builder.toString(); - } - - /** - * Returns the field path element that refers to the provided field descriptor. - * - * @param fieldDescriptor The field descriptor to generate a field path element for. - * @return The field path element that corresponds to the provided field descriptor. - */ - static FieldPathElement fieldPathElement(Descriptors.FieldDescriptor fieldDescriptor) { - String name; - if (fieldDescriptor.isExtension()) { - name = "[" + fieldDescriptor.getFullName() + "]"; - } else { - name = fieldDescriptor.getName(); - } - return FieldPathElement.newBuilder() - .setFieldNumber(fieldDescriptor.getNumber()) - .setFieldName(name) - .setFieldType(fieldDescriptor.getType().toProto()) - .build(); - } - - /** - * Provided a list of violations, adjusts it by prepending rule and field path elements. - * - * @param violations A list of violations. - * @param fieldPathElement A field path element to prepend, or null. - * @param rulePathElements Rule path elements to prepend. - * @return For convenience, the list of violations passed into the violations parameter. - */ - static List updatePaths( - List violations, - @Nullable FieldPathElement fieldPathElement, - List rulePathElements) { - if (fieldPathElement != null || !rulePathElements.isEmpty()) { - for (RuleViolation.Builder violation : violations) { - for (int i = rulePathElements.size() - 1; i >= 0; i--) { - violation.addFirstRulePathElement(rulePathElements.get(i)); - } - if (fieldPathElement != null) { - violation.addFirstFieldPathElement(fieldPathElement); - } - } - } - return violations; - } -} diff --git a/src/main/java/build/buf/protovalidate/Format.java b/src/main/java/build/buf/protovalidate/Format.java deleted file mode 100644 index af476f83e..000000000 --- a/src/main/java/build/buf/protovalidate/Format.java +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static java.time.format.DateTimeFormatter.ISO_INSTANT; - -import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.Duration; -import com.google.protobuf.Timestamp; -import dev.cel.common.types.TypeType; -import dev.cel.common.values.CelByteString; -import dev.cel.runtime.CelEvaluationException; -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.text.DecimalFormat; -import java.time.Instant; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.stream.Collectors; - -/** String formatter for CEL evaluation. */ -final class Format { - /** - * Format the string with a {@link List}. - * - * @param fmtString the string to format. - * @param list the arguments. - * @return the formatted string. - * @throws CelEvaluationException If an error occurs formatting the string. - */ - static String format(String fmtString, List list) throws CelEvaluationException { - // StringBuilder to accumulate the formatted string - StringBuilder builder = new StringBuilder(); - int index = 0; - int argIndex = 0; - while (index < fmtString.length()) { - char c = fmtString.charAt(index++); - if (c != '%') { - // Append non-format characters directly - builder.append(c); - // Add the entire character if it's not a UTF-8 character. - if ((c & 0x80) != 0) { - // Add the rest of the UTF-8 character. - while (index < fmtString.length() && (fmtString.charAt(index) & 0xc0) == 0x80) { - builder.append(fmtString.charAt(index++)); - } - } - continue; - } - if (index >= fmtString.length()) { - throw new CelEvaluationException("format: expected format specifier"); - } - if (fmtString.charAt(index) == '%') { - // Escaped '%', append '%' and move to the next character - builder.append('%'); - index++; - continue; - } - if (argIndex >= list.size()) { - throw new CelEvaluationException("index " + argIndex + " out of range"); - } - Object arg = list.get(argIndex++); - c = fmtString.charAt(index++); - int precision = 6; - if (c == '.') { - // parse the precision - precision = 0; - while (index < fmtString.length() - && '0' <= fmtString.charAt(index) - && fmtString.charAt(index) <= '9') { - precision = precision * 10 + (fmtString.charAt(index++) - '0'); - } - if (index >= fmtString.length()) { - throw new CelEvaluationException("format: expected format specifier"); - } - c = fmtString.charAt(index++); - } - - switch (c) { - case 'd': - builder.append(formatDecimal(arg)); - break; - case 'x': - builder.append(formatHex(arg)); - break; - case 'X': - // We can use a root locale, because the only characters are hex (A-F). - builder.append(formatHex(arg).toUpperCase(Locale.ROOT)); - break; - case 's': - builder.append(formatString(arg)); - break; - case 'e': - builder.append(formatExponential(arg, precision)); - break; - case 'f': - builder.append(formatFloat(arg, precision)); - break; - case 'b': - builder.append(formatBinary(arg)); - break; - case 'o': - builder.append(formatOctal(arg)); - break; - default: - throw new CelEvaluationException( - "could not parse formatting clause: unrecognized formatting clause \"" + c + "\""); - } - } - return builder.toString(); - } - - private static String formatString(Object val) throws CelEvaluationException { - if (val instanceof String) { - return (String) val; - } else if (val instanceof TypeType) { - return ((TypeType) val).containingTypeName(); - } else if (val instanceof Boolean) { - return Boolean.toString((Boolean) val); - } else if (val instanceof Long || val instanceof UnsignedLong) { - Optional str = validateNumber(val); - return str.orElseGet(val::toString); - } else if (val instanceof CelByteString) { - String byteStr = ((CelByteString) val).toStringUtf8(); - // Collapse any contiguous placeholders into one - return byteStr.replaceAll("\\ufffd+", "\ufffd"); - } else if (val instanceof Double) { - Optional result = validateNumber(val); - if (result.isPresent()) { - return result.get(); - } - return formatDecimal(val); - } else if (val instanceof Duration) { - return formatDuration((Duration) val); - } else if (val instanceof java.time.Duration) { - return formatJavaDuration((java.time.Duration) val); - } else if (val instanceof Timestamp) { - return formatTimestamp((Timestamp) val); - } else if (val instanceof java.time.Instant) { - return formatInstant((java.time.Instant) val); - } else if (val instanceof List) { - return formatList((List) val); - } else if (val instanceof Map) { - return formatMap((Map) val); - } else if (val == null || val instanceof dev.cel.common.values.NullValue) { - return "null"; - } - throw new CelEvaluationException( - "error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given " - + val.getClass()); - } - - /** - * Formats a list value. - * - * @param val the value to format. - */ - private static String formatList(List val) throws CelEvaluationException { - StringBuilder builder = new StringBuilder(); - builder.append('['); - - Iterator iter = val.iterator(); - while (iter.hasNext()) { - Object v = iter.next(); - builder.append(formatString(v)); - if (iter.hasNext()) { - builder.append(", "); - } - } - builder.append(']'); - return builder.toString(); - } - - private static String formatMap(Map val) throws CelEvaluationException { - StringBuilder builder = new StringBuilder(); - builder.append('{'); - - SortedMap sorted = new TreeMap<>(); - - for (Entry entry : val.entrySet()) { - sorted.put(formatString(entry.getKey()), formatString(entry.getValue())); - } - - String result = - sorted.entrySet().stream() - .map(entry -> entry.getKey() + ": " + entry.getValue()) - .collect(Collectors.joining(", ")); - - builder.append(result).append('}'); - - return builder.toString(); - } - - private static String formatTimestamp(Timestamp timestamp) { - return ISO_INSTANT.format(Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos())); - } - - private static String formatInstant(Instant instant) { - return ISO_INSTANT.format(instant); - } - - private static String formatDuration(Duration duration) { - StringBuilder builder = new StringBuilder(); - - double totalSeconds = duration.getSeconds() + (duration.getNanos() / 1_000_000_000.0); - - DecimalFormat formatter = new DecimalFormat("0.#########"); - builder.append(formatter.format(totalSeconds)); - builder.append("s"); - - return builder.toString(); - } - - private static String formatJavaDuration(java.time.Duration duration) { - StringBuilder builder = new StringBuilder(); - double totalSeconds = duration.getSeconds() + (duration.getNano() / 1_000_000_000.0); - - DecimalFormat formatter = new DecimalFormat("0.#########"); - builder.append(formatter.format(totalSeconds)); - builder.append("s"); - - return builder.toString(); - } - - private static String formatHex(Object val) throws CelEvaluationException { - if (val instanceof Long) { - return Long.toHexString((Long) val); - } else if (val instanceof UnsignedLong) { - return Long.toHexString(((UnsignedLong) val).longValue()); - } else if (val instanceof CelByteString) { - StringBuilder hexString = new StringBuilder(); - byte[] celBytes = ((CelByteString) val).toByteArray(); - for (byte b : celBytes) { - hexString.append(String.format("%02x", b)); - } - return hexString.toString(); - } else if (val instanceof String) { - String arg = (String) val; - return String.format("%x", new BigInteger(1, arg.getBytes(StandardCharsets.UTF_8))); - } else { - throw new CelEvaluationException( - "error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given " - + val.getClass()); - } - } - - private static String formatDecimal(Object val) throws CelEvaluationException { - if (val instanceof Long || val instanceof UnsignedLong || val instanceof Double) { - Optional str = validateNumber(val); - if (str.isPresent()) { - return str.get(); - } - DecimalFormat formatter = new DecimalFormat("0.#########"); - return formatter.format(val); - } else { - throw new CelEvaluationException( - "error during formatting: decimal clause can only be used on integers, was given " - + val.getClass()); - } - } - - private static String formatOctal(Object val) throws CelEvaluationException { - if (val instanceof Long) { - return Long.toOctalString((Long) val); - } else if (val instanceof UnsignedLong) { - return Long.toOctalString(((UnsignedLong) val).longValue()); - } else { - throw new CelEvaluationException( - "error during formatting: octal clause can only be used on integers, was given " - + val.getClass()); - } - } - - private static String formatBinary(Object val) throws CelEvaluationException { - if (val instanceof Long) { - return Long.toBinaryString((Long) val); - } else if (val instanceof UnsignedLong) { - return Long.toBinaryString(((UnsignedLong) val).longValue()); - } else if (val instanceof Boolean) { - return Boolean.TRUE.equals(val) ? "1" : "0"; - } else { - throw new CelEvaluationException( - "error during formatting: only integers and bools can be formatted as binary, was given " - + val.getClass()); - } - } - - private static String formatExponential(Object val, int precision) throws CelEvaluationException { - if (val instanceof Double) { - Optional str = validateNumber(val); - if (str.isPresent()) { - return str.get(); - } - String pattern = "%." + precision + "e"; - return String.format(pattern, val); - } else { - throw new CelEvaluationException( - "error during formatting: scientific clause can only be used on doubles, was given " - + val.getClass()); - } - } - - private static String formatFloat(Object val, int precision) throws CelEvaluationException { - if (val instanceof Double) { - Optional str = validateNumber(val); - if (str.isPresent()) { - return str.get(); - } - StringBuilder pattern = new StringBuilder("0."); - if (precision > 0) { - for (int i = 0; i < precision; i++) { - pattern.append("0"); - } - } else { - pattern.append("########"); - } - DecimalFormat formatter = new DecimalFormat(pattern.toString()); - return formatter.format(val); - } else { - throw new CelEvaluationException( - "error during formatting: fixed-point clause can only be used on doubles, was given " - + val.getClass()); - } - } - - private static Optional validateNumber(Object val) { - if (val instanceof Double) { - if ((Double) val == Double.POSITIVE_INFINITY) { - return Optional.of("Infinity"); - } else if ((Double) val == Double.NEGATIVE_INFINITY) { - return Optional.of("-Infinity"); - } - } - return Optional.empty(); - } -} diff --git a/src/main/java/build/buf/protovalidate/Ipv4.java b/src/main/java/build/buf/protovalidate/Ipv4.java deleted file mode 100644 index 74bed89df..000000000 --- a/src/main/java/build/buf/protovalidate/Ipv4.java +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import java.util.ArrayList; -import java.util.List; - -/** - * Ipv4 is a class used to parse a given string to determine if it is an IPv4 address or address - * prefix. - */ -final class Ipv4 { - private final String str; - private int index; - private final List octets; - private int prefixLen; - - Ipv4(String str) { - this.str = str; - this.octets = new ArrayList<>(); - } - - /** - * Returns the 32-bit value of an address parsed through address() or addressPrefix(). - * - *

Returns -1 if no address was parsed successfully. - */ - int getBits() { - if (this.octets.size() != 4) { - return -1; - } - return (this.octets.get(0) << 24) - | (this.octets.get(1) << 16) - | (this.octets.get(2) << 8) - | this.octets.get(3); - } - - /** - * Returns true if all bits to the right of the prefix-length are all zeros. - * - *

Behavior is undefined if addressPrefix() has not been called before, or has returned false. - */ - boolean isPrefixOnly() { - int bits = this.getBits(); - - int mask = 0; - if (this.prefixLen == 32) { - mask = 0xffffffff; - } else { - mask = ~(0xffffffff >>> this.prefixLen); - } - - int masked = bits & mask; - - return bits == masked; - } - - // Parses an IPv4 Address in dotted decimal notation. - boolean address() { - return this.addressPart() && this.index == this.str.length(); - } - - // Parses an IPv4 Address prefix. - boolean addressPrefix() { - return this.addressPart() - && this.take('/') - && this.prefixLength() - && this.index == this.str.length(); - } - - // Store value in prefixLen - private boolean prefixLength() { - int start = this.index; - - while (this.index < this.str.length() && this.digit()) { - if (this.index - start > 2) { - // max prefix-length is 32 bits, so anything more than 2 digits is invalid - return false; - } - } - - String str = this.str.substring(start, this.index); - if (str.isEmpty()) { - // too short - return false; - } - - if (str.length() > 1 && str.charAt(0) == '0') { - // bad leading 0 - return false; - } - - try { - int val = Integer.parseInt(str); - - if (val > 32) { - // max 32 bits - return false; - } - - this.prefixLen = val; - return true; - } catch (NumberFormatException nfe) { - return false; - } - } - - private boolean addressPart() { - int start = this.index; - - if (this.decOctet() - && this.take('.') - && this.decOctet() - && this.take('.') - && this.decOctet() - && this.take('.') - && this.decOctet()) { - return true; - } - - this.index = start; - - return false; - } - - private boolean decOctet() { - int start = this.index; - - while (this.index < this.str.length() && this.digit()) { - if (this.index - start > 3) { - // decimal octet can be three characters at most - return false; - } - } - - String str = this.str.substring(start, this.index); - if (str.isEmpty()) { - // too short - return false; - } - - if (str.length() > 1 && str.charAt(0) == '0') { - // bad leading 0 - return false; - } - - try { - int val = Integer.parseInt(str); - - if (val > 255) { - return false; - } - - this.octets.add((short) val); - - return true; - } catch (NumberFormatException nfe) { - // Error converting to number - return false; - } - } - - /** - * Determines whether the current position is a digit. - * - *

Parses the rule: - * - *

DIGIT = %x30-39 ; 0-9
-   */
-  private boolean digit() {
-    char c = this.str.charAt(this.index);
-    if ('0' <= c && c <= '9') {
-      this.index++;
-      return true;
-    }
-    return false;
-  }
-
-  /** Take the given char at the current position, incrementing the index if necessary. */
-  private boolean take(char c) {
-    if (this.index >= this.str.length()) {
-      return false;
-    }
-
-    if (this.str.charAt(this.index) == c) {
-      this.index++;
-      return true;
-    }
-
-    return false;
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/Ipv6.java b/src/main/java/build/buf/protovalidate/Ipv6.java
deleted file mode 100644
index b1679939b..000000000
--- a/src/main/java/build/buf/protovalidate/Ipv6.java
+++ /dev/null
@@ -1,361 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Ipv6 is a class used to parse a given string to determine if it is an IPv6 address or address
- * prefix.
- */
-final class Ipv6 {
-  private final String str;
-  private int index;
-  // 16-bit pieces found
-  private final List pieces;
-  // number of 16-bit pieces found when double colon was found
-  private int doubleColonAt;
-  private boolean doubleColonSeen;
-  // dotted notation for right-most 32 bits
-  private String dottedRaw;
-  // dotted notation successfully parsed as IPv4
-  @Nullable private Ipv4 dottedAddr;
-  private boolean zoneIDFound;
-  // 0 - 128
-  private int prefixLen;
-
-  Ipv6(String str) {
-    this.str = str;
-    this.pieces = new ArrayList<>();
-    this.doubleColonAt = -1;
-    this.dottedRaw = "";
-  }
-
-  /**
-   * Returns the 128-bit value of an address parsed through address() or addressPrefix() as a
-   * 2-element length array of 64-bit values.
-   *
-   * 

Returns [0L, 0L] if no address was parsed successfully. - */ - private long[] getBits() { - List p16 = this.pieces; - - // handle dotted decimal, add to p16 - if (this.dottedAddr != null) { - // right-most 32 bits - long dotted32 = this.dottedAddr.getBits(); - // high 16 bits - p16.add((int) (dotted32 >> 16)); - // low 16 bits - p16.add((int) dotted32); - } - - // handle double colon, fill pieces with 0 - if (this.doubleColonSeen) { - while (p16.size() < 8) { - p16.add(this.doubleColonAt, 0x00000000); - } - } - - if (p16.size() != 8) { - return new long[] {0L, 0L}; - } - - return new long[] { - Long.valueOf(p16.get(0)) << 48 - | Long.valueOf(p16.get(1)) << 32 - | Long.valueOf(p16.get(2)) << 16 - | Long.valueOf(p16.get(3)), - Long.valueOf(p16.get(4)) << 48 - | Long.valueOf(p16.get(5)) << 32 - | Long.valueOf(p16.get(6)) << 16 - | Long.valueOf(p16.get(7)) - }; - } - - boolean isPrefixOnly() { - // For each 64-bit piece of the address, require that values to the right of the prefix are zero - long[] bits = this.getBits(); - for (int i = 0; i < bits.length; i++) { - long p64 = bits[i]; - long size = this.prefixLen - 64L * i; - - long mask = 0L; - if (size >= 64) { - mask = 0xFFFFFFFFFFFFFFFFL; - } else if (size < 0) { - mask = 0x0; - } else { - mask = ~(0xFFFFFFFFFFFFFFFFL >>> size); - } - long masked = p64 & mask; - if (p64 != masked) { - return false; - } - } - - return true; - } - - // Parses an IPv6 Address following RFC 4291, with optional zone id following RFC 4007. - boolean address() { - return this.addressPart() && this.index == this.str.length(); - } - - // Parse IPv6 Address Prefix following RFC 4291. Zone id is not permitted. - boolean addressPrefix() { - return this.addressPart() - && !this.zoneIDFound - && this.take('/') - && this.prefixLength() - && this.index == this.str.length(); - } - - // Stores value in prefixLen - private boolean prefixLength() { - int start = this.index; - - while (this.index < this.str.length() && this.digit()) { - if (this.index - start > 3) { - return false; - } - } - - String str = this.str.substring(start, this.index); - - if (str.isEmpty()) { - // too short - return false; - } - - if (str.length() > 1 && str.charAt(0) == '0') { - // bad leading 0 - return false; - } - - try { - int val = Integer.parseInt(str); - - if (val > 128) { - // max 128 bits - return false; - } - - this.prefixLen = val; - return true; - } catch (NumberFormatException nfe) { - // Error converting to number - return false; - } - } - - // Stores dotted notation for right-most 32 bits in dottedRaw / dottedAddr if found. - private boolean addressPart() { - while (this.index < this.str.length()) { - // dotted notation for right-most 32 bits, e.g. 0:0:0:0:0:ffff:192.1.56.10 - if ((this.doubleColonSeen || this.pieces.size() == 6) && this.dotted()) { - Ipv4 dotted = new Ipv4(this.dottedRaw); - if (dotted.address()) { - this.dottedAddr = dotted; - return true; - } - return false; - } - - try { - if (this.h16()) { - continue; - } - } catch (IllegalStateException | NumberFormatException e) { - return false; - } - - if (this.take(':')) { - if (this.take(':')) { - if (this.doubleColonSeen) { - return false; - } - - this.doubleColonSeen = true; - this.doubleColonAt = this.pieces.size(); - if (this.take(':')) { - return false; - } - } else if (this.index == 1 || this.index == this.str.length()) { - // invalid - string cannot start or end on single colon - return false; - } - continue; - } - - if (this.str.charAt(this.index) == '%' && !this.zoneID()) { - return false; - } - - break; - } - - int totalPieces = this.pieces.size(); - if (this.doubleColonSeen) { - return totalPieces < 8; - } - return totalPieces == 8; - } - - /** - * There is no definition for the character set allowed in the zone identifier. RFC 4007 permits - * basically any non-null string. - * - *

RFC 6874: ZoneID = 1*( unreserved / pct-encoded )
-   */
-  private boolean zoneID() {
-    int start = this.index;
-
-    if (this.take('%')) {
-      if (this.str.length() - this.index > 0) {
-        // permit any non-null string
-        this.index = this.str.length();
-        this.zoneIDFound = true;
-
-        return true;
-      }
-    }
-
-    this.index = start;
-    this.zoneIDFound = false;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a dotted address.
-   *
-   * 

Parses the rule: - * - *

1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
-   *
-   * 

Stores match in dottedRaw. - */ - private boolean dotted() { - int start = this.index; - - this.dottedRaw = ""; - - while (this.index < this.str.length() && (this.digit() || this.take('.'))) {} - - if (this.index - start >= 7) { - this.dottedRaw = this.str.substring(start, this.index); - - return true; - } - - this.index = start; - - return false; - } - - /** - * Determines whether the current position is an h16. - * - *

Parses the rule: - * - *

h16 = 1*4HEXDIG
-   *
-   * If 1-4 hex digits are found, the parsed 16-bit unsigned integer is stored
-   * in pieces and true is returned.
-   * If 0 hex digits are found, returns false.
-   * If more than 4 hex digits are found, an IllegalStateException is thrown.
-   * If the found hex digits cannot be converted to an int, a NumberFormatException is raised.
-   */
-  private boolean h16() throws IllegalStateException, NumberFormatException {
-    int start = this.index;
-
-    while (this.index < this.str.length() && this.hexDig()) {}
-
-    String str = this.str.substring(start, this.index);
-
-    if (str.isEmpty()) {
-      // too short, just return false
-      // this is not an error condition, it just means we didn't find any
-      // hex digits at the current position.
-      return false;
-    }
-
-    if (str.length() > 4) {
-      // too long
-      // this is an error condition, it means we found a string of more than
-      // four valid hex digits, which is invalid in ipv6 addresses.
-      throw new IllegalStateException("invalid hex");
-    }
-
-    // Note that this will throw a NumberFormatException if string cannot be
-    // converted to an int.
-    int val = Integer.parseInt(str, 16);
-
-    this.pieces.add(val);
-    return true;
-  }
-
-  /**
-   * Determines whether the current position is a hex digit.
-   *
-   * 

Parses the rule: - * - *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
-   */
-  private boolean hexDig() {
-    char c = this.str.charAt(this.index);
-
-    if (('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')) {
-      this.index++;
-
-      return true;
-    }
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a digit.
-   *
-   * 

Parses the rule: - * - *

DIGIT = %x30-39 ; 0-9
-   */
-  private boolean digit() {
-    char c = this.str.charAt(this.index);
-    if ('0' <= c && c <= '9') {
-      this.index++;
-      return true;
-    }
-    return false;
-  }
-
-  /** Take the given char at the current position, incrementing the index if necessary. */
-  private boolean take(char c) {
-    if (this.index >= this.str.length()) {
-      return false;
-    }
-
-    if (this.str.charAt(this.index) == c) {
-      this.index++;
-      return true;
-    }
-
-    return false;
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/ListElementValue.java b/src/main/java/build/buf/protovalidate/ListElementValue.java
deleted file mode 100644
index 8e397d328..000000000
--- a/src/main/java/build/buf/protovalidate/ListElementValue.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import com.google.protobuf.Descriptors;
-import com.google.protobuf.Message;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import org.jspecify.annotations.Nullable;
-
-/**
- * The {@link Value} type that contains a field descriptor for repeated field and the value of an
- * element.
- */
-final class ListElementValue implements Value {
-  /** Object type since the object type is inferred from the field descriptor. */
-  private final Object value;
-
-  /**
-   * {@link com.google.protobuf.Descriptors.FieldDescriptor} is the field descriptor for the value.
-   */
-  private final Descriptors.FieldDescriptor fieldDescriptor;
-
-  ListElementValue(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
-    this.value = value;
-    this.fieldDescriptor = fieldDescriptor;
-  }
-
-  @Override
-  public Descriptors.@Nullable FieldDescriptor fieldDescriptor() {
-    return fieldDescriptor;
-  }
-
-  @Override
-  public @Nullable Message messageValue() {
-    if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
-      return (Message) value;
-    }
-    return null;
-  }
-
-  @Override
-  public  T value(Class clazz) {
-    Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType();
-    if (type == Descriptors.FieldDescriptor.Type.MESSAGE) {
-      return clazz.cast(value);
-    }
-    return clazz.cast(ProtoAdapter.scalarToCel(type, value));
-  }
-
-  @Override
-  public Object rawValue() {
-    return value;
-  }
-
-  @Override
-  public List repeatedValue() {
-    return Collections.emptyList();
-  }
-
-  @Override
-  public Map mapValue() {
-    return Collections.emptyMap();
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/ListEvaluator.java b/src/main/java/build/buf/protovalidate/ListEvaluator.java
deleted file mode 100644
index 6b389ede8..000000000
--- a/src/main/java/build/buf/protovalidate/ListEvaluator.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import build.buf.protovalidate.exceptions.ExecutionException;
-import build.buf.validate.FieldPath;
-import build.buf.validate.FieldPathElement;
-import build.buf.validate.FieldRules;
-import build.buf.validate.RepeatedRules;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/** Performs validation on the elements of a repeated field. */
-final class ListEvaluator implements Evaluator {
-  /** Rule path to repeated rules */
-  private static final FieldPath REPEATED_ITEMS_RULE_PATH =
-      FieldPath.newBuilder()
-          .addElements(
-              FieldPathUtils.fieldPathElement(
-                  FieldRules.getDescriptor().findFieldByNumber(FieldRules.REPEATED_FIELD_NUMBER)))
-          .addElements(
-              FieldPathUtils.fieldPathElement(
-                  RepeatedRules.getDescriptor()
-                      .findFieldByNumber(RepeatedRules.ITEMS_FIELD_NUMBER)))
-          .build();
-
-  private final RuleViolationHelper helper;
-
-  /** Rules are checked on every item of the list. */
-  final ValueEvaluator itemRules;
-
-  /** Constructs a {@link ListEvaluator}. */
-  ListEvaluator(ValueEvaluator valueEvaluator) {
-    this.helper = new RuleViolationHelper(valueEvaluator);
-    this.itemRules = new ValueEvaluator(null, REPEATED_ITEMS_RULE_PATH);
-  }
-
-  @Override
-  public boolean tautology() {
-    return itemRules.tautology();
-  }
-
-  @Override
-  public List evaluate(Value val, boolean failFast)
-      throws ExecutionException {
-    List allViolations = new ArrayList<>();
-    List repeatedValues = val.repeatedValue();
-    for (int i = 0; i < repeatedValues.size(); i++) {
-      List violations = itemRules.evaluate(repeatedValues.get(i), failFast);
-      if (violations.isEmpty()) {
-        continue;
-      }
-      FieldPathElement fieldPathElement =
-          Objects.requireNonNull(helper.getFieldPathElement()).toBuilder().setIndex(i).build();
-      FieldPathUtils.updatePaths(violations, fieldPathElement, helper.getRulePrefixElements());
-      if (failFast && !violations.isEmpty()) {
-        return violations;
-      }
-      allViolations.addAll(violations);
-    }
-    return allViolations;
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/MapEvaluator.java b/src/main/java/build/buf/protovalidate/MapEvaluator.java
deleted file mode 100644
index bc418e977..000000000
--- a/src/main/java/build/buf/protovalidate/MapEvaluator.java
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import build.buf.protovalidate.exceptions.ExecutionException;
-import build.buf.validate.FieldPath;
-import build.buf.validate.FieldPathElement;
-import build.buf.validate.FieldRules;
-import build.buf.validate.MapRules;
-import com.google.protobuf.Descriptors;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/** Performs validation on a map field's key-value pairs. */
-final class MapEvaluator implements Evaluator {
-  /** Rule path to map key rules */
-  private static final FieldPath MAP_KEYS_RULE_PATH =
-      FieldPath.newBuilder()
-          .addElements(
-              FieldPathUtils.fieldPathElement(
-                  FieldRules.getDescriptor().findFieldByNumber(FieldRules.MAP_FIELD_NUMBER)))
-          .addElements(
-              FieldPathUtils.fieldPathElement(
-                  MapRules.getDescriptor().findFieldByNumber(MapRules.KEYS_FIELD_NUMBER)))
-          .build();
-
-  /** Rule path to map value rules */
-  private static final FieldPath MAP_VALUES_RULE_PATH =
-      FieldPath.newBuilder()
-          .addElements(
-              FieldPathUtils.fieldPathElement(
-                  FieldRules.getDescriptor().findFieldByNumber(FieldRules.MAP_FIELD_NUMBER)))
-          .addElements(
-              FieldPathUtils.fieldPathElement(
-                  MapRules.getDescriptor().findFieldByNumber(MapRules.VALUES_FIELD_NUMBER)))
-          .build();
-
-  private final RuleViolationHelper helper;
-
-  /** Rule for checking the map keys */
-  private final ValueEvaluator keyEvaluator;
-
-  /** Rule for checking the map values */
-  private final ValueEvaluator valueEvaluator;
-
-  /** Field descriptor of the map field */
-  final Descriptors.FieldDescriptor fieldDescriptor;
-
-  /** Field descriptor of the map key field */
-  final Descriptors.FieldDescriptor keyFieldDescriptor;
-
-  /** Field descriptor of the map value field */
-  final Descriptors.FieldDescriptor valueFieldDescriptor;
-
-  /**
-   * Constructs a {@link MapEvaluator}.
-   *
-   * @param valueEvaluator The value evaluator this rule exists under.
-   */
-  MapEvaluator(ValueEvaluator valueEvaluator, Descriptors.FieldDescriptor fieldDescriptor) {
-    this.helper = new RuleViolationHelper(valueEvaluator);
-    this.keyEvaluator = new ValueEvaluator(null, MAP_KEYS_RULE_PATH);
-    this.valueEvaluator = new ValueEvaluator(null, MAP_VALUES_RULE_PATH);
-    this.fieldDescriptor = fieldDescriptor;
-    this.keyFieldDescriptor = fieldDescriptor.getMessageType().findFieldByNumber(1);
-    this.valueFieldDescriptor = fieldDescriptor.getMessageType().findFieldByNumber(2);
-  }
-
-  /**
-   * Gets the key evaluator associated with this map evaluator.
-   *
-   * @return The key evaluator.
-   */
-  ValueEvaluator getKeyEvaluator() {
-    return keyEvaluator;
-  }
-
-  /**
-   * Gets the value evaluator associated with this map evaluator.
-   *
-   * @return The value evaluator.
-   */
-  ValueEvaluator getValueEvaluator() {
-    return valueEvaluator;
-  }
-
-  @Override
-  public boolean tautology() {
-    return keyEvaluator.tautology() && valueEvaluator.tautology();
-  }
-
-  @Override
-  public List evaluate(Value val, boolean failFast)
-      throws ExecutionException {
-    List violations = new ArrayList<>();
-    Map mapValue = val.mapValue();
-    for (Map.Entry entry : mapValue.entrySet()) {
-      violations.addAll(evalPairs(entry.getKey(), entry.getValue(), failFast));
-      if (failFast && !violations.isEmpty()) {
-        return violations;
-      }
-    }
-    if (violations.isEmpty()) {
-      return RuleViolation.NO_VIOLATIONS;
-    }
-    return violations;
-  }
-
-  private List evalPairs(Value key, Value value, boolean failFast)
-      throws ExecutionException {
-    List keyViolations =
-        keyEvaluator.evaluate(key, failFast).stream()
-            .map(violation -> violation.setForKey(true))
-            .collect(Collectors.toList());
-    final List valueViolations;
-    if (failFast && !keyViolations.isEmpty()) {
-      // Don't evaluate value rules if failFast is enabled and keys failed validation.
-      // We still need to continue execution to the end to properly prefix violation field paths.
-      valueViolations = RuleViolation.NO_VIOLATIONS;
-    } else {
-      valueViolations = valueEvaluator.evaluate(value, failFast);
-    }
-    if (keyViolations.isEmpty() && valueViolations.isEmpty()) {
-      return Collections.emptyList();
-    }
-    List violations =
-        new ArrayList<>(keyViolations.size() + valueViolations.size());
-    violations.addAll(keyViolations);
-    violations.addAll(valueViolations);
-
-    FieldPathElement.Builder fieldPathElementBuilder =
-        Objects.requireNonNull(helper.getFieldPathElement()).toBuilder();
-    fieldPathElementBuilder.setKeyType(keyFieldDescriptor.getType().toProto());
-    fieldPathElementBuilder.setValueType(valueFieldDescriptor.getType().toProto());
-    switch (keyFieldDescriptor.getType().toProto()) {
-      case TYPE_INT64:
-      case TYPE_INT32:
-      case TYPE_SINT32:
-      case TYPE_SINT64:
-      case TYPE_SFIXED32:
-      case TYPE_SFIXED64:
-        fieldPathElementBuilder.setIntKey(key.value(Number.class).longValue());
-        break;
-      case TYPE_UINT32:
-      case TYPE_UINT64:
-      case TYPE_FIXED32:
-      case TYPE_FIXED64:
-        fieldPathElementBuilder.setUintKey(key.value(Number.class).longValue());
-        break;
-      case TYPE_BOOL:
-        fieldPathElementBuilder.setBoolKey(key.value(Boolean.class));
-        break;
-      case TYPE_STRING:
-        fieldPathElementBuilder.setStringKey(key.value(String.class));
-        break;
-      default:
-        throw new ExecutionException("Unexpected map key type");
-    }
-    FieldPathElement fieldPathElement = fieldPathElementBuilder.build();
-    return FieldPathUtils.updatePaths(violations, fieldPathElement, helper.getRulePrefixElements());
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/MapRulesEvaluator.java b/src/main/java/build/buf/protovalidate/MapRulesEvaluator.java
deleted file mode 100644
index 39455b9e8..000000000
--- a/src/main/java/build/buf/protovalidate/MapRulesEvaluator.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import build.buf.validate.FieldRules;
-import build.buf.validate.MapRules;
-import com.google.protobuf.Descriptors.FieldDescriptor;
-import java.util.List;
-import org.jspecify.annotations.Nullable;
-
-/**
- * Native evaluator for map-level rules: {@code min_pairs} and {@code max_pairs}. Key/value rules
- * continue to flow through {@link build.buf.protovalidate.MapEvaluator} and the inner key/value
- * {@link build.buf.protovalidate.ValueEvaluator}s. Mirrors {@code nativeMapEval} in
- * protovalidate-go's {@code native_map.go}.
- */
-final class MapRulesEvaluator implements Evaluator {
-  private static final FieldDescriptor MAP_RULES_DESC =
-      FieldRules.getDescriptor().findFieldByNumber(FieldRules.MAP_FIELD_NUMBER);
-
-  private static final RuleSite MIN_PAIRS_SITE =
-      RuleSite.of(
-          MAP_RULES_DESC,
-          MapRules.getDescriptor().findFieldByNumber(MapRules.MIN_PAIRS_FIELD_NUMBER),
-          "map.min_pairs",
-          null);
-  private static final RuleSite MAX_PAIRS_SITE =
-      RuleSite.of(
-          MAP_RULES_DESC,
-          MapRules.getDescriptor().findFieldByNumber(MapRules.MAX_PAIRS_FIELD_NUMBER),
-          "map.max_pairs",
-          null);
-
-  private final RuleBase base;
-  private final @Nullable Long minPairs;
-  private final @Nullable Long maxPairs;
-
-  private MapRulesEvaluator(RuleBase base, @Nullable Long minPairs, @Nullable Long maxPairs) {
-    this.base = base;
-    this.minPairs = minPairs;
-    this.maxPairs = maxPairs;
-  }
-
-  static @Nullable Evaluator tryBuild(RuleBase base, FieldRules.Builder rulesBuilder) {
-    if (!rulesBuilder.hasMap()) {
-      return null;
-    }
-    MapRules rules = rulesBuilder.getMap();
-    if (!rules.getUnknownFields().isEmpty()) {
-      return null;
-    }
-
-    MapRules.Builder mb = rules.toBuilder();
-    boolean hasRule = false;
-
-    Long minPairs = null;
-    if (rules.hasMinPairs()) {
-      minPairs = rules.getMinPairs();
-      mb.clearMinPairs();
-      hasRule = true;
-    }
-
-    Long maxPairs = null;
-    if (rules.hasMaxPairs()) {
-      maxPairs = rules.getMaxPairs();
-      mb.clearMaxPairs();
-      hasRule = true;
-    }
-
-    if (!hasRule) {
-      return null;
-    }
-    rulesBuilder.setMap(mb.build());
-    return new MapRulesEvaluator(base, minPairs, maxPairs);
-  }
-
-  @Override
-  public boolean tautology() {
-    // tryBuild returns null when neither field is set, so this evaluator is never built
-    // without at least one rule active. Match the rest of the rules package by returning false
-    // unconditionally.
-    return false;
-  }
-
-  @Override
-  public List evaluate(Value val, boolean failFast) {
-    // Java protobuf returns map fields as a List of synthetic key/value entry messages; the size
-    // is the pair count.
-    List entries = (List) val.rawValue();
-    long size = entries.size();
-    List violations = null;
-
-    if (minPairs != null && size < minPairs) {
-      violations =
-          RuleBase.add(
-              violations,
-              NativeViolations.newViolation(
-                  MIN_PAIRS_SITE,
-                  null,
-                  "map must be at least " + minPairs + " entries",
-                  val,
-                  minPairs));
-      if (failFast) return base.done(violations);
-    }
-
-    if (maxPairs != null && size > maxPairs) {
-      violations =
-          RuleBase.add(
-              violations,
-              NativeViolations.newViolation(
-                  MAX_PAIRS_SITE,
-                  null,
-                  "map must be at most " + maxPairs + " entries",
-                  val,
-                  maxPairs));
-      if (failFast) return base.done(violations);
-    }
-
-    return base.done(violations);
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java b/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java
deleted file mode 100644
index aecce4e92..000000000
--- a/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import build.buf.protovalidate.exceptions.ExecutionException;
-import com.google.protobuf.Descriptors.FieldDescriptor;
-import com.google.protobuf.Message;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * A specialized {@link Evaluator} for applying {@code buf.validate.MessageOneofRule} to a {@link
- * com.google.protobuf.Message}.
- */
-final class MessageOneofEvaluator implements Evaluator {
-  /** List of fields that are part of the oneof */
-  final List fields;
-
-  /** If at least one must be set. */
-  final boolean required;
-
-  MessageOneofEvaluator(List fields, boolean required) {
-    this.fields = fields;
-    this.required = required;
-  }
-
-  @Override
-  public boolean tautology() {
-    return false;
-  }
-
-  @Override
-  public List evaluate(Value val, boolean failFast)
-      throws ExecutionException {
-    Message msg = val.messageValue();
-    if (msg == null) {
-      return RuleViolation.NO_VIOLATIONS;
-    }
-    int hasCount = 0;
-    for (FieldDescriptor field : fields) {
-      if (FieldEvaluator.isFieldSet(msg, field)) {
-        hasCount++;
-      }
-    }
-    if (hasCount > 1) {
-      return Collections.singletonList(
-          RuleViolation.newBuilder()
-              .setRuleId("message.oneof")
-              .setMessage(String.format("only one of %s can be set", fieldNames())));
-    }
-    if (this.required && hasCount == 0) {
-      return Collections.singletonList(
-          RuleViolation.newBuilder()
-              .setRuleId("message.oneof")
-              .setMessage(String.format("one of %s must be set", fieldNames())));
-    }
-    return Collections.emptyList();
-  }
-
-  String fieldNames() {
-    return fields.stream().map(FieldDescriptor::getName).collect(Collectors.joining(", "));
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/NativeViolations.java b/src/main/java/build/buf/protovalidate/NativeViolations.java
deleted file mode 100644
index da345b8f1..000000000
--- a/src/main/java/build/buf/protovalidate/NativeViolations.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import org.jspecify.annotations.Nullable;
-
-/**
- * Builds {@link RuleViolation.Builder} instances for native rule evaluators.
- *
- * 

The resulting builder carries only rule-relative state: rule id, message, the rule path suffix - * from {@link RuleSite}, and optional field/rule values. Field path and any nested-rule prefix are - * prepended later by {@code FieldPathUtils.updatePaths} when the violations leave the native - * evaluator's {@code evaluate} — the same pattern {@code CelPrograms} uses. - */ -final class NativeViolations { - private NativeViolations() {} - - /** - * Builds a violation for a rule failure. If {@link RuleSite#getRuleId()} or {@link - * RuleSite#getMessage()} return non-null they take precedence over the supplied {@code ruleId} - * and {@code message} arguments; this lets a {@link RuleSite} pre-bake constant text and skip the - * per-call argument when there's nothing dynamic to report. - * - * @param site the rule site (rule path suffix + leaf descriptor + optional pre-baked id/message) - * @param ruleId rule id to use when the site doesn't have one pre-baked - * @param message violation message to use when the site doesn't have one pre-baked - * @param fieldValue the failing field value (its descriptor is used to populate {@code - * field_value} on the violation); pass null to omit - * @param ruleValue the rule's bound value (e.g. the {@code 5} in {@code min_len = 5}), bound to - * the site's leaf descriptor; pass null to omit - */ - static RuleViolation.Builder newViolation( - RuleSite site, - @Nullable String ruleId, - @Nullable String message, - @Nullable Value fieldValue, - @Nullable Object ruleValue) { - String effectiveRuleId = (site.getRuleId() != null) ? site.getRuleId() : ruleId; - String effectiveMessage = (site.getMessage() != null) ? site.getMessage() : message; - - RuleViolation.Builder builder = RuleViolation.newBuilder(); - if (effectiveRuleId != null) { - builder.setRuleId(effectiveRuleId); - } - if (effectiveMessage != null) { - builder.setMessage(effectiveMessage); - } - builder.addAllRulePathElements(site.getPathElements()); - if (fieldValue != null && fieldValue.fieldDescriptor() != null) { - builder.setFieldValue(new RuleViolation.FieldValue(fieldValue)); - } - if (ruleValue != null) { - builder.setRuleValue(new RuleViolation.FieldValue(ruleValue, site.getLeafDescriptor())); - } - return builder; - } -} diff --git a/src/main/java/build/buf/protovalidate/NowVariable.java b/src/main/java/build/buf/protovalidate/NowVariable.java deleted file mode 100644 index 8ece15336..000000000 --- a/src/main/java/build/buf/protovalidate/NowVariable.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import com.google.protobuf.Timestamp; -import dev.cel.runtime.CelVariableResolver; -import java.time.Instant; -import java.util.Optional; -import org.jspecify.annotations.Nullable; - -/** - * {@link NowVariable} implements {@link CelVariableResolver}, providing a lazily produced timestamp - * for accessing the variable `now` that's constant within an evaluation. - */ -final class NowVariable implements CelVariableResolver { - /** The name of the 'now' variable. */ - static final String NOW_NAME = "now"; - - /** The resolved value of the 'now' variable. */ - @Nullable private Timestamp now; - - /** Creates an instance of a "now" variable. */ - NowVariable() {} - - @Override - public Optional find(String name) { - if (!name.equals(NOW_NAME)) { - return Optional.empty(); - } - if (this.now == null) { - Instant nowInstant = Instant.now(); - now = - Timestamp.newBuilder() - .setSeconds(nowInstant.getEpochSecond()) - .setNanos(nowInstant.getNano()) - .build(); - } - return Optional.of(this.now); - } -} diff --git a/src/main/java/build/buf/protovalidate/NumericDescriptors.java b/src/main/java/build/buf/protovalidate/NumericDescriptors.java deleted file mode 100644 index b54870387..000000000 --- a/src/main/java/build/buf/protovalidate/NumericDescriptors.java +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import org.jspecify.annotations.Nullable; - -/** - * Pre-built {@link RuleSite}s for a single numeric rule type ({@code Int32Rules}, {@code - * UInt64Rules}, {@code FloatRules}, etc.). Built once per type at class-init time so violation - * construction at validation time avoids re-building path-element protos. - * - *

{@code finiteSite} is null for non-float kinds. - */ -final class NumericDescriptors { - final RuleSite gtSite; - final RuleSite gteSite; - final RuleSite ltSite; - final RuleSite lteSite; - final RuleSite constSite; - final RuleSite inSite; - final RuleSite notInSite; - final @Nullable RuleSite finiteSite; - // Leaf descriptors used to look up rule fields on the *Rules message at build time. - final FieldDescriptor gtField; - final FieldDescriptor gteField; - final FieldDescriptor ltField; - final FieldDescriptor lteField; - final FieldDescriptor constField; - final FieldDescriptor inField; - final FieldDescriptor notInField; - final @Nullable FieldDescriptor finiteField; - - private NumericDescriptors( - RuleSite gtSite, - RuleSite gteSite, - RuleSite ltSite, - RuleSite lteSite, - RuleSite constSite, - RuleSite inSite, - RuleSite notInSite, - @Nullable RuleSite finiteSite, - FieldDescriptor gtField, - FieldDescriptor gteField, - FieldDescriptor ltField, - FieldDescriptor lteField, - FieldDescriptor constField, - FieldDescriptor inField, - FieldDescriptor notInField, - @Nullable FieldDescriptor finiteField) { - this.gtSite = gtSite; - this.gteSite = gteSite; - this.ltSite = ltSite; - this.lteSite = lteSite; - this.constSite = constSite; - this.inSite = inSite; - this.notInSite = notInSite; - this.finiteSite = finiteSite; - this.gtField = gtField; - this.gteField = gteField; - this.ltField = ltField; - this.lteField = lteField; - this.constField = constField; - this.inField = inField; - this.notInField = notInField; - this.finiteField = finiteField; - } - - /** - * Builds the descriptor bundle for a numeric rule type. - * - * @param fieldRulesField the {@link FieldDescriptor} of the {@code FieldRules} oneof case (e.g. - * the {@code int32} field on {@code FieldRules}) - * @param rulesDescriptor the {@link Descriptor} of the rules message (e.g. {@code Int32Rules}) - * @param typeName the proto rule prefix used in rule ids (e.g. {@code "int32"}) - * @param hasFinite whether this kind supports the {@code finite} rule (only float/double do) - */ - static NumericDescriptors build( - FieldDescriptor fieldRulesField, - Descriptor rulesDescriptor, - String typeName, - boolean hasFinite) { - FieldDescriptor gt = rulesDescriptor.findFieldByName("gt"); - FieldDescriptor gte = rulesDescriptor.findFieldByName("gte"); - FieldDescriptor lt = rulesDescriptor.findFieldByName("lt"); - FieldDescriptor lte = rulesDescriptor.findFieldByName("lte"); - FieldDescriptor constant = rulesDescriptor.findFieldByName("const"); - FieldDescriptor inField = rulesDescriptor.findFieldByName("in"); - FieldDescriptor notInField = rulesDescriptor.findFieldByName("not_in"); - FieldDescriptor finiteField = hasFinite ? rulesDescriptor.findFieldByName("finite") : null; - return new NumericDescriptors( - // Sites carry rule-id and rule-path for violation building. Where the rule id is - // computed dynamically (gt/gte/lt/lte combine into different ids depending on which - // bounds are active), pass null and let the caller supply per-violation. - RuleSite.of(fieldRulesField, gt, null, null), - RuleSite.of(fieldRulesField, gte, null, null), - RuleSite.of(fieldRulesField, lt, null, null), - RuleSite.of(fieldRulesField, lte, null, null), - RuleSite.of(fieldRulesField, constant, typeName + ".const", null), - RuleSite.of(fieldRulesField, inField, typeName + ".in", null), - RuleSite.of(fieldRulesField, notInField, typeName + ".not_in", null), - finiteField != null - ? RuleSite.of(fieldRulesField, finiteField, typeName + ".finite", "must be finite") - : null, - gt, - gte, - lt, - lte, - constant, - inField, - notInField, - finiteField); - } -} diff --git a/src/main/java/build/buf/protovalidate/NumericRulesEvaluator.java b/src/main/java/build/buf/protovalidate/NumericRulesEvaluator.java deleted file mode 100644 index 8e81cd951..000000000 --- a/src/main/java/build/buf/protovalidate/NumericRulesEvaluator.java +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import org.jspecify.annotations.Nullable; - -/** - * Native evaluator for the standard numeric rules ({@code gt}, {@code gte}, {@code lt}, {@code - * lte}, {@code const}, {@code in}, {@code not_in}, plus {@code finite} for float/double). Mirrors - * {@code nativeNumericCompare} in protovalidate-go's {@code native_numeric.go}, parameterized over - * the boxed Java numeric type ({@code Integer}, {@code Long}, {@code Float}, {@code Double}). - * - *

Signed vs unsigned semantics are encoded in the supplied {@link NumericTypeConfig}: the - * config's comparator decides ordering and its formatter decides how values render in messages. The - * same {@code Integer}-typed evaluator is shared between {@code int32} (signed comparator, {@code - * String.valueOf} formatter) and {@code uint32} ({@code Integer::compareUnsigned}, {@code - * Integer::toUnsignedString}). - */ -final class NumericRulesEvaluator> implements Evaluator { - - /** Lower bound active on this evaluator. */ - enum LowerBound { - NONE, - GTE, // inclusive - GT // exclusive - } - - /** Upper bound active on this evaluator. */ - enum UpperBound { - NONE, - LT, - LTE - } - - private final RuleBase base; - private final NumericTypeConfig config; - private final @Nullable T constVal; - private final List inVals; - private final List notInVals; - private final @Nullable T loVal; - private final LowerBound lowerKind; - private final @Nullable T hiVal; - private final UpperBound upperKind; - private final boolean finite; - - private NumericRulesEvaluator( - RuleBase base, - NumericTypeConfig config, - @Nullable T constVal, - List inVals, - List notInVals, - @Nullable T loVal, - LowerBound lowerKind, - @Nullable T hiVal, - UpperBound upperKind, - boolean finite) { - this.base = base; - this.config = config; - this.constVal = constVal; - this.inVals = inVals; - this.notInVals = notInVals; - this.loVal = loVal; - this.lowerKind = lowerKind; - this.hiVal = hiVal; - this.upperKind = upperKind; - this.finite = finite; - } - - /** - * Attempts to build a {@link NumericRulesEvaluator} for the rules under {@code rulesField} on - * {@code rulesBuilder}. Returns null when the typed sub-message is unset, has unknown fields, or - * carries no rule we cover. On success, clears the covered fields on the builder so CEL doesn't - * also compile programs for them. - * - * @param base the base rule evaluator. - * @param rulesBuilder the builder for the rules sub-message. - * @param config the config for the numeric type this evaluator is for. - * @return a new evaluator, or null if the sub-message is unset, has unknown fields, or carries no - * rule we cover. - */ - static > @Nullable Evaluator tryBuild( - RuleBase base, FieldRules.Builder rulesBuilder, NumericTypeConfig config) { - FieldDescriptor rulesField = config.rulesField; - if (!rulesBuilder.hasField(rulesField)) { - return null; - } - Message rulesMsg = (Message) rulesBuilder.getField(rulesField); - if (!rulesMsg.getUnknownFields().isEmpty()) { - return null; - } - - NumericDescriptors descs = config.descriptors; - Message.Builder typedBuilder = rulesMsg.toBuilder(); - boolean hasRule = false; - - // if a loVal or hiVal is NaN, exit out because it's not a valid rule - T loVal = null; - LowerBound lowerKind = LowerBound.NONE; - if (rulesMsg.hasField(descs.gtField)) { - lowerKind = LowerBound.GT; - loVal = config.valueClass.cast(rulesMsg.getField(descs.gtField)); - if (loVal instanceof Float && Float.isNaN((Float) loVal)) { - return null; - } - if (loVal instanceof Double && Double.isNaN((Double) loVal)) { - return null; - } - typedBuilder.clearField(descs.gtField); - hasRule = true; - } else if (rulesMsg.hasField(descs.gteField)) { - lowerKind = LowerBound.GTE; - loVal = config.valueClass.cast(rulesMsg.getField(descs.gteField)); - if (loVal instanceof Float && Float.isNaN((Float) loVal)) { - return null; - } - if (loVal instanceof Double && Double.isNaN((Double) loVal)) { - return null; - } - typedBuilder.clearField(descs.gteField); - hasRule = true; - } - - T hiVal = null; - UpperBound upperKind = UpperBound.NONE; - if (rulesMsg.hasField(descs.ltField)) { - upperKind = UpperBound.LT; - hiVal = config.valueClass.cast(rulesMsg.getField(descs.ltField)); - if (hiVal instanceof Float && Float.isNaN((Float) hiVal)) { - return null; - } - if (hiVal instanceof Double && Double.isNaN((Double) hiVal)) { - return null; - } - typedBuilder.clearField(descs.ltField); - hasRule = true; - } else if (rulesMsg.hasField(descs.lteField)) { - upperKind = UpperBound.LTE; - hiVal = config.valueClass.cast(rulesMsg.getField(descs.lteField)); - if (hiVal instanceof Float && Float.isNaN((Float) hiVal)) { - return null; - } - if (hiVal instanceof Double && Double.isNaN((Double) hiVal)) { - return null; - } - typedBuilder.clearField(descs.lteField); - hasRule = true; - } - - T constVal = null; - if (rulesMsg.hasField(descs.constField)) { - constVal = config.valueClass.cast(rulesMsg.getField(descs.constField)); - typedBuilder.clearField(descs.constField); - hasRule = true; - } - - @SuppressWarnings("unchecked") - List rawInVals = (List) rulesMsg.getField(descs.inField); - List inVals = rawInVals.isEmpty() ? Collections.emptyList() : rawInVals; - if (!inVals.isEmpty()) { - typedBuilder.clearField(descs.inField); - hasRule = true; - } - - @SuppressWarnings("unchecked") - List rawNotInVals = (List) rulesMsg.getField(descs.notInField); - List notInVals = rawNotInVals.isEmpty() ? Collections.emptyList() : rawNotInVals; - if (!notInVals.isEmpty()) { - typedBuilder.clearField(descs.notInField); - hasRule = true; - } - - boolean finite = false; - if (descs.finiteField != null && rulesMsg.hasField(descs.finiteField)) { - finite = (Boolean) rulesMsg.getField(descs.finiteField); - typedBuilder.clearField(descs.finiteField); - hasRule = true; - } - - if (!hasRule) { - return null; - } - rulesBuilder.setField(rulesField, typedBuilder.build()); - return new NumericRulesEvaluator( - base, config, constVal, inVals, notInVals, loVal, lowerKind, hiVal, upperKind, finite); - } - - @Override - public boolean tautology() { - return false; - } - - @Override - public List evaluate(Value val, boolean failFast) { - T actual = config.valueClass.cast(val.rawValue()); - List violations = null; - - if (constVal != null && config.comparator.compare(actual, constVal) != 0) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - config.descriptors.constSite, - null, - "must equal " + config.formatter.apply(constVal), - val, - constVal)); - if (failFast) { - return base.done(violations); - } - } - - if (!inVals.isEmpty() && !containsValue(inVals, actual)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - config.descriptors.inSite, - null, - "must be in list " + formatList(inVals), - val, - inVals)); - if (failFast) { - return base.done(violations); - } - } - - if (!notInVals.isEmpty() && containsValue(notInVals, actual)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - config.descriptors.notInSite, - null, - "must not be in list " + formatList(notInVals), - val, - notInVals)); - if (failFast) { - return base.done(violations); - } - } - - if (finite && !isFinite(actual)) { - // descriptors.finiteSite is non-null whenever finite==true (set up at builder time). - RuleSite site = - Objects.requireNonNull( - config.descriptors.finiteSite, "finiteSite must be set when finite is true"); - violations = - RuleBase.add(violations, NativeViolations.newViolation(site, null, null, val, true)); - if (failFast) { - return base.done(violations); - } - } - - if (lowerKind != LowerBound.NONE || upperKind != UpperBound.NONE) { - RuleViolation.Builder rangeViolation = buildRangeViolation(val, actual); - if (rangeViolation != null) { - violations = RuleBase.add(violations, rangeViolation); - if (failFast) { - return base.done(violations); - } - } - } - - return base.done(violations); - } - - // --- Per-rule violation builders --- - - /** - * Builds a violation for the lower/upper bound check, or returns null if the value is in range. - * Mirrors {@code nativeNumericCompare.evaluateRange} in protovalidate-go, including the - * exclusive-range semantics where {@code gt > lt} (or equivalents) means "value not in [lt, gt]". - */ - private RuleViolation.@Nullable Builder buildRangeViolation(Value val, T actual) { - boolean isNaN = config.nanFailsRange && isNaN(actual); - if (lowerKind == LowerBound.NONE) { - if (isNaN || aboveHi(actual)) { - return NativeViolations.newViolation( - hiSite(), gtltRule(), "must be " + hiMessage(), val, hiVal); - } - return null; - } - if (upperKind == UpperBound.NONE) { - if (isNaN || belowLo(actual)) { - return NativeViolations.newViolation( - loSite(), gtltRule(), "must be " + loMessage(), val, loVal); - } - return null; - } - boolean failure; - if (isNormalRange()) { - failure = isNaN || aboveHi(actual) || belowLo(actual); - } else { - failure = isNaN || (aboveHi(actual) && belowLo(actual)); - } - if (failure) { - String message = "must be " + loMessage() + " " + conjunction() + " " + hiMessage(); - return NativeViolations.newViolation(loSite(), gtltRule(), message, val, loVal); - } - return null; - } - - // --- Comparison helpers (depend on the comparator from config) --- - - private boolean belowLo(T value) { - int cmp = config.comparator.compare(value, loVal); - return lowerKind == LowerBound.GT ? cmp <= 0 : cmp < 0; - } - - private boolean aboveHi(T value) { - int cmp = config.comparator.compare(value, hiVal); - return upperKind == UpperBound.LT ? cmp >= 0 : cmp > 0; - } - - private boolean isNormalRange() { - // hi >= lo means a normal range. For unsigned kinds this uses the unsigned comparator. - return config.comparator.compare(hiVal, loVal) >= 0; - } - - private boolean containsValue(List list, T value) { - // Use the comparator for equality so unsigned/signed semantics agree. Java's List.contains - // would use Object.equals, which is fine for boxed primitives but we keep a single source of - // truth. - for (T t : list) { - if (config.comparator.compare(t, value) == 0) { - return true; - } - } - return false; - } - - private static boolean isFinite(T value) { - if (value instanceof Float) { - return Float.isFinite(value.floatValue()); - } - if (value instanceof Double) { - return Double.isFinite(value.doubleValue()); - } - // Integer kinds are always finite. - return true; - } - - private static boolean isNaN(T value) { - if (value instanceof Float) { - return Float.isNaN(value.floatValue()); - } - if (value instanceof Double) { - return Double.isNaN(value.doubleValue()); - } - return false; - } - - // --- Rule-id and message helpers (mirror Go's gtltRule / loMessage / hiMessage / conjunction) - // --- - - private RuleSite loSite() { - return lowerKind == LowerBound.GT ? config.descriptors.gtSite : config.descriptors.gteSite; - } - - private RuleSite hiSite() { - return upperKind == UpperBound.LT ? config.descriptors.ltSite : config.descriptors.lteSite; - } - - private String gtRulePrefix() { - return lowerKind == LowerBound.GT ? config.typeName + ".gt" : config.typeName + ".gte"; - } - - private String ltRulePrefix() { - return upperKind == UpperBound.LT ? config.typeName + ".lt" : config.typeName + ".lte"; - } - - /** Combined rule id, e.g. {@code int32.gt_lt_exclusive}. Mirrors Go's {@code gtltRule}. */ - private String gtltRule() { - if (lowerKind == LowerBound.NONE) { - return ltRulePrefix(); - } - String prefix = gtRulePrefix(); - if (upperKind == UpperBound.LT) { - prefix += "_lt"; - if (!isNormalRange()) { - prefix += "_exclusive"; - } - } else if (upperKind == UpperBound.LTE) { - prefix += "_lte"; - if (!isNormalRange()) { - prefix += "_exclusive"; - } - } - return prefix; - } - - private String loMessage() { - String formatted = config.formatter.apply(loVal); - return lowerKind == LowerBound.GT - ? "greater than " + formatted - : "greater than or equal to " + formatted; - } - - private String hiMessage() { - String formatted = config.formatter.apply(hiVal); - return upperKind == UpperBound.LT - ? "less than " + formatted - : "less than or equal to " + formatted; - } - - private String conjunction() { - return isNormalRange() ? "and" : "or"; - } - - /** Renders {@code vals} using this kind's typed formatter. */ - private String formatList(List vals) { - return RuleBase.formatList(vals, config.formatter); - } -} diff --git a/src/main/java/build/buf/protovalidate/NumericTypeConfig.java b/src/main/java/build/buf/protovalidate/NumericTypeConfig.java deleted file mode 100644 index acf887271..000000000 --- a/src/main/java/build/buf/protovalidate/NumericTypeConfig.java +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.DoubleRules; -import build.buf.validate.FieldRules; -import build.buf.validate.Fixed32Rules; -import build.buf.validate.Fixed64Rules; -import build.buf.validate.FloatRules; -import build.buf.validate.Int32Rules; -import build.buf.validate.Int64Rules; -import build.buf.validate.SFixed32Rules; -import build.buf.validate.SFixed64Rules; -import build.buf.validate.SInt32Rules; -import build.buf.validate.SInt64Rules; -import build.buf.validate.UInt32Rules; -import build.buf.validate.UInt64Rules; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.math.BigDecimal; -import java.util.Comparator; -import java.util.function.Function; - -/** - * Per-kind type config for native numeric rule evaluation. Bundles everything that varies between - * proto numeric kinds (int32, uint32, float, etc.): descriptor lookups, the boxed Java type, the - * comparator (signed vs unsigned), the value-to-string formatter, and a flag for whether NaN fails - * range checks. - * - *

One static instance per kind, shared across every {@link NumericRulesEvaluator} for that kind. - */ -final class NumericTypeConfig> { - /** Proto rule prefix used in rule ids ({@code "int32"}, {@code "uint64"}, etc.). */ - final String typeName; - - /** Pre-built rule sites and field descriptors for this kind. */ - final NumericDescriptors descriptors; - - /** Boxed Java class for values of this kind. Used to extract values from {@code Value}. */ - final Class valueClass; - - /** - * Comparator over values of this kind. For signed/float kinds this is the natural order; for - * unsigned kinds it is {@code Integer.compareUnsigned}/{@code Long.compareUnsigned} since Java - * stores unsigned protobuf values in signed primitives. - */ - final Comparator comparator; - - /** - * Renders a value to the string used in violation messages. {@code String::valueOf} for signed - * and float kinds; {@code Integer::toUnsignedString}/{@code Long::toUnsignedString} for unsigned. - * Critical for unsigned kinds — {@code String.valueOf(int)} would print a negative integer for - * values whose unsigned representation exceeds {@code Integer.MAX_VALUE}. - */ - final Function formatter; - - /** - * True for {@code float}/{@code double}: NaN fails range checks (matches CEL semantics). False - * for integer kinds — they have no NaN. - */ - final boolean nanFailsRange; - - /** - * The {@link FieldRules} field descriptor for this numeric kind (e.g. the {@code int32} field on - * {@code FieldRules}). Used by the dispatcher to read and clear the typed rules sub-message. - */ - final FieldDescriptor rulesField; - - private NumericTypeConfig( - String typeName, - NumericDescriptors descriptors, - Class valueClass, - Comparator comparator, - Function formatter, - boolean nanFailsRange, - FieldDescriptor rulesField) { - this.typeName = typeName; - this.descriptors = descriptors; - this.valueClass = valueClass; - this.comparator = comparator; - this.formatter = formatter; - this.nanFailsRange = nanFailsRange; - this.rulesField = rulesField; - } - - // --- Static configs, one per proto numeric kind --- - // - // Class-init invariant: every static config below transitively calls FieldRules.getDescriptor() - // and the per-kind *Rules.getDescriptor() through frField() and NumericDescriptors.build(). For - // class-loading to succeed, FieldRules and the per-kind rules messages must be loadable when - // NumericTypeConfig is. They are — FieldRules is touched by every entry into EvaluatorBuilder, - // and the per-kind rules messages live in the same generated bundle. If a future change moves - // NumericTypeConfig's initialization earlier (e.g. via a static reference from a class loaded - // before FieldRules), expect NoClassDefFoundError on this class. - - private static FieldDescriptor frField(int number) { - return FieldRules.getDescriptor().findFieldByNumber(number); - } - - /** Builds a NumericTypeConfig and exposes the FieldRules-level field descriptor on it. */ - private static > NumericTypeConfig create( - String typeName, - int fieldRulesFieldNumber, - com.google.protobuf.Descriptors.Descriptor rulesDescriptor, - Class valueClass, - Comparator comparator, - Function formatter, - boolean nanFailsRange) { - FieldDescriptor rulesField = frField(fieldRulesFieldNumber); - return new NumericTypeConfig<>( - typeName, - NumericDescriptors.build(rulesField, rulesDescriptor, typeName, nanFailsRange), - valueClass, - comparator, - formatter, - nanFailsRange, - rulesField); - } - - static final NumericTypeConfig INT32 = - create( - "int32", - FieldRules.INT32_FIELD_NUMBER, - Int32Rules.getDescriptor(), - Integer.class, - Integer::compare, - String::valueOf, - false); - - static final NumericTypeConfig SINT32 = - create( - "sint32", - FieldRules.SINT32_FIELD_NUMBER, - SInt32Rules.getDescriptor(), - Integer.class, - Integer::compare, - String::valueOf, - false); - - static final NumericTypeConfig SFIXED32 = - create( - "sfixed32", - FieldRules.SFIXED32_FIELD_NUMBER, - SFixed32Rules.getDescriptor(), - Integer.class, - Integer::compare, - String::valueOf, - false); - - static final NumericTypeConfig UINT32 = - create( - "uint32", - FieldRules.UINT32_FIELD_NUMBER, - UInt32Rules.getDescriptor(), - Integer.class, - Integer::compareUnsigned, - Integer::toUnsignedString, - false); - - static final NumericTypeConfig FIXED32 = - create( - "fixed32", - FieldRules.FIXED32_FIELD_NUMBER, - Fixed32Rules.getDescriptor(), - Integer.class, - Integer::compareUnsigned, - Integer::toUnsignedString, - false); - - static final NumericTypeConfig INT64 = - create( - "int64", - FieldRules.INT64_FIELD_NUMBER, - Int64Rules.getDescriptor(), - Long.class, - Long::compare, - String::valueOf, - false); - - static final NumericTypeConfig SINT64 = - create( - "sint64", - FieldRules.SINT64_FIELD_NUMBER, - SInt64Rules.getDescriptor(), - Long.class, - Long::compare, - String::valueOf, - false); - - static final NumericTypeConfig SFIXED64 = - create( - "sfixed64", - FieldRules.SFIXED64_FIELD_NUMBER, - SFixed64Rules.getDescriptor(), - Long.class, - Long::compare, - String::valueOf, - false); - - static final NumericTypeConfig UINT64 = - create( - "uint64", - FieldRules.UINT64_FIELD_NUMBER, - UInt64Rules.getDescriptor(), - Long.class, - Long::compareUnsigned, - Long::toUnsignedString, - false); - - static final NumericTypeConfig FIXED64 = - create( - "fixed64", - FieldRules.FIXED64_FIELD_NUMBER, - Fixed64Rules.getDescriptor(), - Long.class, - Long::compareUnsigned, - Long::toUnsignedString, - false); - - static final NumericTypeConfig FLOAT = - create( - "float", - FieldRules.FLOAT_FIELD_NUMBER, - FloatRules.getDescriptor(), - Float.class, - NumericTypeConfig::floatCompare, - NumericTypeConfig::floatFormatter, - true); - - static final NumericTypeConfig DOUBLE = - create( - "double", - FieldRules.DOUBLE_FIELD_NUMBER, - DoubleRules.getDescriptor(), - Double.class, - NumericTypeConfig::doubleCompare, - NumericTypeConfig::doubleFormatter, - true); - - // Float and double comparators treat +0.0 and -0.0 as equal, matching IEEE-754. NaN keeps - // Java's compareTo semantics (NaN.compareTo(NaN) == 0) so behavior matches the existing - // protovalidate-java CEL path, which uses the same Object.equals semantics. - - private static int floatCompare(Float f1, Float f2) { - if (f1 == 0.0f && f2 == 0.0f) { - return 0; - } - // NaN != Nan, but Java thinks it does. - if (f1.isNaN() && f2.isNaN()) { - return -1; - } - return f1.compareTo(f2); - } - - private static int doubleCompare(Double d1, Double d2) { - if (d1 == 0.0 && d2 == 0.0) { - return 0; - } - // NaN != Nan, but Java thinks it does. - if (d1.isNaN() && d2.isNaN()) { - return -1; - } - return d1.compareTo(d2); - } - - private static final int FLOAT_NEG_ZERO_BITS = Float.floatToIntBits(-0.0f); - private static final long DOUBLE_NEG_ZERO_BITS = Double.doubleToLongBits(-0.0); - - private static String floatFormatter(Float f) { - if (Float.floatToIntBits(f) == FLOAT_NEG_ZERO_BITS) { - return "-0"; - } - if (f.isNaN()) { - return "NaN"; - } - if (f.isInfinite()) { - if (Math.signum(f) < 0) { - return "-Infinity"; - } - return "Infinity"; - } - // closest way to get to strconv.FormatFloat(d, 'f', -1, 64) in Go - String out = BigDecimal.valueOf(f).toPlainString(); - // cut off .0 at the end for whole numbers - if (out.endsWith(".0")) { - out = out.substring(0, out.length() - 2); - } - return out; - } - - private static String doubleFormatter(Double d) { - if (Double.doubleToLongBits(d) == DOUBLE_NEG_ZERO_BITS) { - return "-0"; - } - if (d.isNaN()) { - return "NaN"; - } - if (d.isInfinite()) { - if (Math.signum(d) < 0) { - return "-Infinity"; - } - return "Infinity"; - } - // closest way to get to strconv.FormatFloat(d, 'f', -1, 64) in Go - String out = BigDecimal.valueOf(d).toPlainString(); - // cut off .0 at the end for whole numbers - if (out.endsWith(".0")) { - out = out.substring(0, out.length() - 2); - } - return out; - } -} diff --git a/src/main/java/build/buf/protovalidate/ProtoAdapter.java b/src/main/java/build/buf/protovalidate/ProtoAdapter.java deleted file mode 100644 index 1ed924577..000000000 --- a/src/main/java/build/buf/protovalidate/ProtoAdapter.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import com.google.common.primitives.UnsignedLong; -import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ByteString; -import com.google.protobuf.Descriptors; -import com.google.protobuf.Message; -import com.google.protobuf.Timestamp; -import dev.cel.common.values.CelByteString; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * CEL supports protobuf natively but when we pass it field values (like scalars, repeated, and - * maps) it has no way to treat them like a proto message field. This class has methods to convert - * to a cel values. - */ -final class ProtoAdapter { - /** Converts a protobuf field value to CEL compatible value. */ - static Object toCel(Descriptors.FieldDescriptor fieldDescriptor, Object value) { - Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType(); - if (fieldDescriptor.isMapField()) { - List input = - value instanceof List - ? (List) value - : Collections.singletonList((AbstractMessage) value); - Descriptors.FieldDescriptor keyDesc = fieldDescriptor.getMessageType().findFieldByNumber(1); - Descriptors.FieldDescriptor valDesc = fieldDescriptor.getMessageType().findFieldByNumber(2); - Map out = new HashMap<>(input.size()); - - for (AbstractMessage entry : input) { - Object keyValue = entry.getField(keyDesc); - Object valValue = entry.getField(valDesc); - out.put(toCel(keyDesc, keyValue), toCel(valDesc, valValue)); - } - return out; - } - if (fieldDescriptor.isRepeated()) { - List list = (List) value; - List out = new ArrayList<>(list.size()); - for (Object element : list) { - out.add(scalarToCel(type, element)); - } - return out; - } - return scalarToCel(type, value); - } - - /** Converts a scalar type to cel value. */ - static Object scalarToCel(Descriptors.FieldDescriptor.Type type, Object value) { - switch (type) { - case BYTES: - if (value instanceof ByteString) { - return CelByteString.of(((ByteString) value).toByteArray()); - } - return value; - case ENUM: - if (value instanceof Descriptors.EnumValueDescriptor) { - return (long) ((Descriptors.EnumValueDescriptor) value).getNumber(); - } - return value; - case FLOAT: - return Double.valueOf((Float) value); - case INT32: - case SINT32: - case SFIXED32: - return Long.valueOf((Integer) value); - case FIXED32: - case UINT32: - return UnsignedLong.fromLongBits(Long.valueOf((Integer) value)); - case UINT64: - case FIXED64: - return UnsignedLong.fromLongBits((Long) value); - case MESSAGE: - // cel-java 0.11.1 added support for java.time.Instant and java.time.Duration. - Message msg = (Message) value; - if (msg instanceof com.google.protobuf.Timestamp) { - Timestamp timestamp = (Timestamp) value; - return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); - } - if (msg instanceof com.google.protobuf.Duration) { - com.google.protobuf.Duration duration = (com.google.protobuf.Duration) value; - return Duration.ofSeconds(duration.getSeconds(), duration.getNanos()); - } - return value; - default: - return value; - } - } -} diff --git a/src/main/java/build/buf/protovalidate/RepeatedRulesEvaluator.java b/src/main/java/build/buf/protovalidate/RepeatedRulesEvaluator.java deleted file mode 100644 index c0a7bd306..000000000 --- a/src/main/java/build/buf/protovalidate/RepeatedRulesEvaluator.java +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldRules; -import build.buf.validate.RepeatedRules; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.List; -import org.jspecify.annotations.Nullable; - -/** - * Native evaluator for repeated list-level rules: {@code min_items}, {@code max_items}, {@code - * unique}. Element-level rules continue to flow through {@code ListEvaluator} and the inner {@link - * build.buf.protovalidate.ValueEvaluator}. - * - *

The {@code unique} rule is only supported when the element kind has well-defined value - * equality (scalars, strings, bytes, bools, enums). Message/group element kinds fall back to CEL. - * Mirrors {@code nativeRepeatedEval} in protovalidate-go's {@code native_repeated.go}. - */ -final class RepeatedRulesEvaluator implements Evaluator { - private static final FieldDescriptor REPEATED_RULES_DESC = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.REPEATED_FIELD_NUMBER); - - private static final RuleSite MIN_ITEMS_SITE = - RuleSite.of( - REPEATED_RULES_DESC, - RepeatedRules.getDescriptor().findFieldByNumber(RepeatedRules.MIN_ITEMS_FIELD_NUMBER), - "repeated.min_items", - null); - private static final RuleSite MAX_ITEMS_SITE = - RuleSite.of( - REPEATED_RULES_DESC, - RepeatedRules.getDescriptor().findFieldByNumber(RepeatedRules.MAX_ITEMS_FIELD_NUMBER), - "repeated.max_items", - null); - private static final RuleSite UNIQUE_SITE = - RuleSite.of( - REPEATED_RULES_DESC, - RepeatedRules.getDescriptor().findFieldByNumber(RepeatedRules.UNIQUE_FIELD_NUMBER), - "repeated.unique", - "repeated value must contain unique items"); - - private final RuleBase base; - private final @Nullable Long minItems; - private final @Nullable Long maxItems; - private final boolean unique; - - private RepeatedRulesEvaluator( - RuleBase base, @Nullable Long minItems, @Nullable Long maxItems, boolean unique) { - this.base = base; - this.minItems = minItems; - this.maxItems = maxItems; - this.unique = unique; - } - - static @Nullable Evaluator tryBuild(RuleBase base, FieldRules.Builder rulesBuilder) { - if (!rulesBuilder.hasRepeated()) { - return null; - } - RepeatedRules rules = rulesBuilder.getRepeated(); - if (!rules.getUnknownFields().isEmpty()) { - return null; - } - - RepeatedRules.Builder rb = rules.toBuilder(); - boolean hasRule = false; - - Long minItems = null; - if (rules.hasMinItems()) { - minItems = rules.getMinItems(); - rb.clearMinItems(); - hasRule = true; - } - - Long maxItems = null; - if (rules.hasMaxItems()) { - maxItems = rules.getMaxItems(); - rb.clearMaxItems(); - hasRule = true; - } - - boolean unique = false; - if (rules.getUnique()) { - // Element kind must support reliable Object.equals — scalars, strings, bools, bytes, enums - // all do (ByteString and EnumValueDescriptor have correct equals/hashCode). Messages don't, - // so fall through to CEL. - FieldDescriptor descriptor = base.getDescriptor(); - if (descriptor == null || !isUniqueSupported(descriptor.getType())) { - return null; - } - unique = true; - rb.clearUnique(); - hasRule = true; - } - - if (!hasRule) { - return null; - } - rulesBuilder.setRepeated(rb.build()); - return new RepeatedRulesEvaluator(base, minItems, maxItems, unique); - } - - private static boolean isUniqueSupported(FieldDescriptor.Type type) { - switch (type) { - case INT32: - case SINT32: - case SFIXED32: - case INT64: - case SINT64: - case SFIXED64: - case UINT32: - case FIXED32: - case UINT64: - case FIXED64: - case FLOAT: - case DOUBLE: - case BOOL: - case STRING: - case BYTES: - case ENUM: - return true; - case MESSAGE: - case GROUP: - default: - return false; - } - } - - @Override - public boolean tautology() { - return false; - } - - @Override - public List evaluate(Value val, boolean failFast) { - List list = (List) val.rawValue(); - long size = list.size(); - List violations = null; - - if (minItems != null && size < minItems) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MIN_ITEMS_SITE, - null, - "must contain at least " + minItems + " item(s)", - val, - minItems)); - if (failFast) return base.done(violations); - } - - if (maxItems != null && size > maxItems) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MAX_ITEMS_SITE, - null, - "must contain no more than " + maxItems + " item(s)", - val, - maxItems)); - if (failFast) return base.done(violations); - } - - if (unique && !CustomOverload.uniqueList(list)) { - violations = - RuleBase.add( - violations, NativeViolations.newViolation(UNIQUE_SITE, null, null, val, true)); - if (failFast) return base.done(violations); - } - - return base.done(violations); - } -} diff --git a/src/main/java/build/buf/protovalidate/RuleBase.java b/src/main/java/build/buf/protovalidate/RuleBase.java deleted file mode 100644 index 1abfd2bf1..000000000 --- a/src/main/java/build/buf/protovalidate/RuleBase.java +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import org.jspecify.annotations.Nullable; - -/** - * Common context shared across native rule evaluators: the field's descriptor, its single - * containing-message field path element, and any nested-rule prefix that must be prepended to rule - * paths in violations. - * - *

Mirrors the {@code base} struct in protovalidate-go's {@code base.go}, adapted to Java's - * existing pattern of letting violations bubble up the call stack with prepended path elements (see - * {@link FieldPathUtils#updatePaths}). - */ -final class RuleBase { - private static final List EMPTY_PREFIX = Collections.emptyList(); - - private final @Nullable FieldDescriptor descriptor; - private final @Nullable FieldPathElement fieldPathElement; - private final @Nullable FieldPath rulePrefix; - - private RuleBase( - @Nullable FieldDescriptor descriptor, - @Nullable FieldPathElement fieldPathElement, - @Nullable FieldPath rulePrefix) { - this.descriptor = descriptor; - this.fieldPathElement = fieldPathElement; - this.rulePrefix = rulePrefix; - } - - /** - * Builds a {@link RuleBase} from the given {@link ValueEvaluator}, computing the field path - * element from its descriptor and capturing its nested-rule prefix. - */ - static RuleBase of(ValueEvaluator valueEvaluator) { - FieldDescriptor desc = valueEvaluator.getDescriptor(); - FieldPathElement fpe = (desc != null) ? FieldPathUtils.fieldPathElement(desc) : null; - return new RuleBase(desc, fpe, valueEvaluator.getNestedRule()); - } - - /** The descriptor of the field being validated, or null when validating a non-field value. */ - @Nullable FieldDescriptor getDescriptor() { - return descriptor; - } - - /** - * The {@link FieldPathElement} for prepending to violation field paths, or null when there is no - * field context (e.g. the value being validated is not a message field). - */ - @Nullable FieldPathElement getFieldPathElement() { - return fieldPathElement; - } - - /** - * The nested-rule path elements (e.g. {@code repeated.items}, {@code map.keys}) to prepend to - * violation rule paths. Empty when there is no nested-rule context. - */ - List getRulePrefixElements() { - if (rulePrefix == null) { - return EMPTY_PREFIX; - } - return rulePrefix.getElementsList(); - } - - // --- Shared violation-list helpers --- - // - // Every native evaluator uses the same lazy null → ArrayList growth pattern and the same - // tail-call to FieldPathUtils.updatePaths. These live here so each evaluator doesn't - // re-implement them. - - /** - * Lazily appends {@code v} to {@code violations}, allocating an {@link ArrayList} only on the - * first append. - */ - static List add( - @Nullable List violations, RuleViolation.Builder v) { - if (violations == null) { - violations = new ArrayList<>(2); - } - violations.add(v); - return violations; - } - - /** - * Finalizes a violation list: returns the empty constant when there's nothing to report, - * otherwise prepends this base's field-path element and rule-prefix elements. - */ - List done(@Nullable List violations) { - if (violations == null || violations.isEmpty()) { - return RuleViolation.NO_VIOLATIONS; - } - return FieldPathUtils.updatePaths(violations, fieldPathElement, getRulePrefixElements()); - } - - /** Renders a list as {@code "[a, b, c]"} using {@code toString} on each element. */ - static String formatList(List vals) { - return formatList(vals, Object::toString); - } - - /** Renders a list as {@code "[a, b, c]"} using {@code formatter} on each element. */ - static String formatList(List vals, Function formatter) { - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < vals.size(); i++) { - if (i > 0) { - sb.append(", "); - } - sb.append(formatter.apply(vals.get(i))); - } - sb.append("]"); - return sb.toString(); - } -} diff --git a/src/main/java/build/buf/protovalidate/RuleCache.java b/src/main/java/build/buf/protovalidate/RuleCache.java deleted file mode 100644 index 9e6962bfd..000000000 --- a/src/main/java/build/buf/protovalidate/RuleCache.java +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.CompilationException; -import build.buf.validate.FieldPath; -import build.buf.validate.FieldRules; -import build.buf.validate.ValidateProto; -import com.google.protobuf.DescriptorProtos; -import com.google.protobuf.Descriptors; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; -import com.google.protobuf.MessageLite; -import com.google.protobuf.TypeRegistry; -import dev.cel.bundle.Cel; -import dev.cel.common.types.StructTypeReference; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime.Program; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.jspecify.annotations.Nullable; - -/** A build-through cache for computed standard rules. */ -final class RuleCache { - private static class CelRule { - final AstExpression astExpression; - final Program program; - final FieldDescriptor field; - final FieldPath rulePath; - - private CelRule( - AstExpression astExpression, Program program, FieldDescriptor field, FieldPath rulePath) { - this.astExpression = astExpression; - this.program = program; - this.field = field; - this.rulePath = rulePath; - } - } - - private static final ExtensionRegistry EXTENSION_REGISTRY = ExtensionRegistry.newInstance(); - - static { - EXTENSION_REGISTRY.add(ValidateProto.predefined); - } - - /** - * Compiled rules keyed by rule field descriptor (e.g. {@code StringRules.min_len}), shared across - * all user fields that reference the same rule. The rule value is bound per call at eval time. - */ - private final Map> descriptorMap = new ConcurrentHashMap<>(); - - /** The environment to use for evaluation. */ - private final Cel cel; - - /** Registry used to resolve dynamic messages. */ - private final TypeRegistry typeRegistry; - - /** Registry used to resolve dynamic extensions. */ - private final ExtensionRegistry extensionRegistry; - - /** Whether to allow unknown rule fields or not. */ - private final boolean allowUnknownFields; - - /** - * Constructs a new build-through cache for the standard rules, with a provided registry to - * resolve dynamic extensions. - * - * @param cel The CEL environment for evaluation. - * @param config The configuration to use for the rule cache. - */ - RuleCache(Cel cel, Config config) { - this.cel = cel; - this.typeRegistry = config.getTypeRegistry(); - this.extensionRegistry = config.getExtensionRegistry(); - this.allowUnknownFields = config.isAllowingUnknownFields(); - } - - /** - * Creates the standard rules for the given field. If forItems is true, the rules for repeated - * list items is built instead of the rules on the list itself. - * - * @param fieldDescriptor The field descriptor to be validated. - * @param fieldRules The field rule that is used for validation. - * @param forItems The field is an item list type. - * @return The list of compiled programs. - * @throws CompilationException If the rules fail to compile. - */ - List compile( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, boolean forItems) - throws CompilationException { - ResolvedRule resolved = resolveRules(fieldDescriptor, fieldRules, forItems); - if (resolved == null) { - // Message null means there were no rules resolved. - return Collections.emptyList(); - } - Message message = resolved.message; - List completeProgramList = new ArrayList<>(); - for (Map.Entry entry : message.getAllFields().entrySet()) { - FieldDescriptor ruleFieldDesc = entry.getKey(); - List programList = - compileRule(fieldDescriptor, forItems, resolved.setOneof, ruleFieldDesc, message); - if (programList == null) continue; - completeProgramList.addAll(programList); - } - List programs = new ArrayList<>(); - for (CelRule rule : completeProgramList) { - Object fieldValue = message.getField(rule.field); - programs.add( - new CompiledProgram( - rule.program, - rule.astExpression.source, - rule.rulePath, - new ObjectValue(rule.field, fieldValue), - Variable.newRuleVariable(message, ProtoAdapter.toCel(rule.field, fieldValue)))); - } - return Collections.unmodifiableList(programs); - } - - private @Nullable List compileRule( - FieldDescriptor fieldDescriptor, - boolean forItems, - FieldDescriptor setOneof, - FieldDescriptor ruleFieldDesc, - Message message) - throws CompilationException { - List celRules = descriptorMap.get(ruleFieldDesc); - if (celRules != null) { - return celRules; - } - build.buf.validate.PredefinedRules rules = getFieldRules(ruleFieldDesc); - if (rules == null) return null; - try { - return descriptorMap.computeIfAbsent( - ruleFieldDesc, - key -> { - try { - return buildCelRules(fieldDescriptor, forItems, setOneof, key, message, rules); - } catch (CompilationException e) { - throw new UncheckedCompilationException(e); - } - }); - } catch (UncheckedCompilationException e) { - throw e.getCompilationException(); - } - } - - private List buildCelRules( - FieldDescriptor fieldDescriptor, - boolean forItems, - FieldDescriptor setOneof, - FieldDescriptor ruleFieldDesc, - Message message, - build.buf.validate.PredefinedRules rules) - throws CompilationException { - List expressions = Expression.fromRules(rules.getCelList()); - List celRules = new ArrayList<>(expressions.size()); - Cel ruleCel = getRuleCel(fieldDescriptor, message, ruleFieldDesc, forItems); - for (Expression expression : expressions) { - FieldPath rulePath = - FieldPath.newBuilder() - .addElements(FieldPathUtils.fieldPathElement(setOneof)) - .addElements(FieldPathUtils.fieldPathElement(ruleFieldDesc)) - .build(); - AstExpression astExpression = AstExpression.newAstExpression(ruleCel, expression); - Program program; - try { - program = ruleCel.createProgram(astExpression.ast); - } catch (CelEvaluationException e) { - throw new CompilationException( - "failed to create program for rule " + astExpression.source.id, e); - } - celRules.add(new CelRule(astExpression, program, ruleFieldDesc, rulePath)); - } - return celRules; - } - - private static final class UncheckedCompilationException extends RuntimeException { - private final CompilationException compilationException; - - UncheckedCompilationException(CompilationException cause) { - super(cause); - this.compilationException = cause; - } - - CompilationException getCompilationException() { - return compilationException; - } - } - - private build.buf.validate.@Nullable PredefinedRules getFieldRules(FieldDescriptor ruleFieldDesc) - throws CompilationException { - DescriptorProtos.FieldOptions options = ruleFieldDesc.getOptions(); - // If the protovalidate field option is unknown, reparse options using our extension registry. - if (options.getUnknownFields().hasField(ValidateProto.predefined.getNumber())) { - try { - options = - DescriptorProtos.FieldOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY); - } catch (InvalidProtocolBufferException e) { - throw new CompilationException("Failed to parse field options", e); - } - } - if (!options.hasExtension(ValidateProto.predefined)) { - return null; - } - Object extensionValue = options.getField(ValidateProto.predefined.getDescriptor()); - build.buf.validate.PredefinedRules rules; - if (extensionValue instanceof build.buf.validate.PredefinedRules) { - rules = (build.buf.validate.PredefinedRules) extensionValue; - } else if (extensionValue instanceof MessageLite) { - // Extension is parsed but with different gencode. We need to reparse it. - try { - rules = - build.buf.validate.PredefinedRules.parseFrom( - ((MessageLite) extensionValue).toByteString()); - } catch (InvalidProtocolBufferException e) { - throw new CompilationException("Failed to parse field rules", e); - } - } else { - // Extension was not a message, just discard it. - return null; - } - return rules; - } - - /** - * Calculates the environment for a specific rule invocation. - * - * @param fieldDescriptor The field descriptor of the field with the rule. - * @param ruleMessage The message of the standard rules. - * @param ruleFieldDesc The field descriptor of the rule. - * @param forItems Whether the field is a list type or not. - * @return An environment with requisite declarations and types added. - */ - private Cel getRuleCel( - FieldDescriptor fieldDescriptor, - Message ruleMessage, - FieldDescriptor ruleFieldDesc, - boolean forItems) { - return cel.toCelBuilder() - .addMessageTypes(ruleMessage.getDescriptorForType()) - .addVar(Variable.THIS_NAME, DescriptorMappings.getCELType(fieldDescriptor, forItems)) - .addVar( - Variable.RULES_NAME, - StructTypeReference.create(ruleMessage.getDescriptorForType().getFullName())) - .addVar(Variable.RULE_NAME, DescriptorMappings.getCELType(ruleFieldDesc, false)) - .build(); - } - - private static class ResolvedRule { - final Message message; - final FieldDescriptor setOneof; - - ResolvedRule(Message message, FieldDescriptor setOneof) { - this.message = message; - this.setOneof = setOneof; - } - } - - /** - * Extracts the standard rules for the specified field. An exception is thrown if the wrong rules - * are applied to a field (typically if there is a type-mismatch). Null is returned if there are - * no standard rules to apply to this field. - */ - @Nullable - private ResolvedRule resolveRules( - FieldDescriptor fieldDescriptor, FieldRules fieldRules, boolean forItems) - throws CompilationException { - // Get the oneof field descriptor from the field rules. - FieldDescriptor oneofFieldDescriptor = - fieldRules.getOneofFieldDescriptor(DescriptorMappings.FIELD_RULES_ONEOF_DESC); - if (oneofFieldDescriptor == null) { - // If the oneof field descriptor is null there are no rules to resolve. - return null; - } - - // Get the expected rule descriptor based on the provided field descriptor and the flag - // indicating whether it is for items. - FieldDescriptor expectedRuleDescriptor = - DescriptorMappings.getExpectedRuleDescriptor(fieldDescriptor, forItems); - - if (expectedRuleDescriptor != null - && !oneofFieldDescriptor.getFullName().equals(expectedRuleDescriptor.getFullName())) { - // If the expected rule does not match the actual oneof rule, throw a - // CompilationError. - throw new CompilationException( - String.format( - "expected rule %s, got %s on field %s", - expectedRuleDescriptor.getName(), - oneofFieldDescriptor.getName(), - fieldDescriptor.getName())); - } - - // If the expected rule descriptor is null or if the field rules do not have the - // oneof field descriptor there are no rules to resolve, so return null. - if (expectedRuleDescriptor == null || !fieldRules.hasField(oneofFieldDescriptor)) { - if (expectedRuleDescriptor == null) { - // The only expected rule descriptor for message fields is for well known types. - // If we didn't find a descriptor and this is a message, there must be a mismatch. - if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - throw new CompilationException( - String.format( - "mismatched message rules, %s is not a valid rule for field %s", - oneofFieldDescriptor.getName(), fieldDescriptor.getName())); - } - } - - return null; - } - - // Get the field from the field rules identified by the oneof field descriptor, casted - // as a Message. - Message typeRules = (Message) fieldRules.getField(oneofFieldDescriptor); - if (!typeRules.getUnknownFields().isEmpty()) { - // If there are unknown fields, try to resolve them using the provided registries. Note that - // we use the type registry to resolve the message descriptor. This is because Java protobuf - // extension resolution relies on descriptor identity. The user's provided type registry can - // provide matching message descriptors for the user's provided extension registry. See the - // documentation for Options.setTypeRegistry for more information. - Descriptors.Descriptor expectedRuleMessageDescriptor = - typeRegistry.find(expectedRuleDescriptor.getMessageType().getFullName()); - if (expectedRuleMessageDescriptor == null) { - expectedRuleMessageDescriptor = expectedRuleDescriptor.getMessageType(); - } - try { - typeRules = - DynamicMessage.parseFrom( - expectedRuleMessageDescriptor, typeRules.toByteString(), extensionRegistry); - } catch (InvalidProtocolBufferException e) { - throw new CompilationException("failed to reparse rules with extension registry", e); - } - } - if (!allowUnknownFields && !typeRules.getUnknownFields().isEmpty()) { - throw new CompilationException("unrecognized field rules"); - } - return new ResolvedRule(typeRules, oneofFieldDescriptor); - } -} diff --git a/src/main/java/build/buf/protovalidate/RuleResolver.java b/src/main/java/build/buf/protovalidate/RuleResolver.java deleted file mode 100644 index fff7dba89..000000000 --- a/src/main/java/build/buf/protovalidate/RuleResolver.java +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.CompilationException; -import build.buf.validate.FieldRules; -import build.buf.validate.MessageRules; -import build.buf.validate.OneofRules; -import build.buf.validate.ValidateProto; -import com.google.protobuf.DescriptorProtos; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Descriptors.OneofDescriptor; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.MessageLite; - -/** Manages the resolution of protovalidate rules. */ -final class RuleResolver { - private static final ExtensionRegistry EXTENSION_REGISTRY = ExtensionRegistry.newInstance(); - - static { - EXTENSION_REGISTRY.add(ValidateProto.message); - EXTENSION_REGISTRY.add(ValidateProto.oneof); - EXTENSION_REGISTRY.add(ValidateProto.field); - } - - /** - * Resolves the rules for a message descriptor. - * - * @param desc the message descriptor. - * @return the resolved {@link MessageRules}. - */ - MessageRules resolveMessageRules(Descriptor desc) - throws InvalidProtocolBufferException, CompilationException { - DescriptorProtos.MessageOptions options = desc.getOptions(); - // If the protovalidate message extension is unknown, reparse using extension registry. - if (options.getUnknownFields().hasField(ValidateProto.message.getNumber())) { - options = - DescriptorProtos.MessageOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY); - } - if (!options.hasExtension(ValidateProto.message)) { - return MessageRules.getDefaultInstance(); - } - // Don't use getExtension here to avoid exception if descriptor types don't match. - // This can occur if the extension is generated to a different Java package. - Object value = options.getField(ValidateProto.message.getDescriptor()); - if (value instanceof MessageRules) { - return ((MessageRules) value); - } - if (value instanceof MessageLite) { - // Possible that this represents the same rule type, just generated to a different - // java_package. - return MessageRules.parseFrom(((MessageLite) value).toByteString()); - } - throw new CompilationException("unexpected message rule option type: " + value); - } - - /** - * Resolves the rules for a oneof descriptor. - * - * @param desc the oneof descriptor. - * @return the resolved {@link OneofRules}. - */ - OneofRules resolveOneofRules(OneofDescriptor desc) - throws InvalidProtocolBufferException, CompilationException { - DescriptorProtos.OneofOptions options = desc.getOptions(); - // If the protovalidate oneof extension is unknown, reparse using extension registry. - if (options.getUnknownFields().hasField(ValidateProto.oneof.getNumber())) { - options = DescriptorProtos.OneofOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY); - } - if (!options.hasExtension(ValidateProto.oneof)) { - return OneofRules.getDefaultInstance(); - } - // Don't use getExtension here to avoid exception if descriptor types don't match. - // This can occur if the extension is generated to a different Java package. - Object value = options.getField(ValidateProto.oneof.getDescriptor()); - if (value instanceof OneofRules) { - return ((OneofRules) value); - } - if (value instanceof MessageLite) { - // Possible that this represents the same rule type, just generated to a different - // java_package. - return OneofRules.parseFrom(((MessageLite) value).toByteString()); - } - throw new CompilationException("unexpected oneof rule option type: " + value); - } - - /** - * Resolves the rules for a field descriptor. - * - * @param desc the field descriptor. - * @return the resolved {@link FieldRules}. - */ - FieldRules resolveFieldRules(FieldDescriptor desc) - throws InvalidProtocolBufferException, CompilationException { - DescriptorProtos.FieldOptions options = desc.getOptions(); - // If the protovalidate field option is unknown, reparse using extension registry. - if (options.getUnknownFields().hasField(ValidateProto.field.getNumber())) { - options = DescriptorProtos.FieldOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY); - } - if (!options.hasExtension(ValidateProto.field)) { - return FieldRules.getDefaultInstance(); - } - // Don't use getExtension here to avoid exception if descriptor types don't match. - // This can occur if the extension is generated to a different Java package. - Object value = options.getField(ValidateProto.field.getDescriptor()); - if (value instanceof FieldRules) { - return ((FieldRules) value); - } - if (value instanceof MessageLite) { - // Possible that this represents the same rule type, just generated to a different - // java_package. - return FieldRules.parseFrom(((MessageLite) value).toByteString()); - } - throw new CompilationException("unexpected field rule option type: " + value); - } -} diff --git a/src/main/java/build/buf/protovalidate/RuleSite.java b/src/main/java/build/buf/protovalidate/RuleSite.java deleted file mode 100644 index aa4a81a8a..000000000 --- a/src/main/java/build/buf/protovalidate/RuleSite.java +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldPathElement; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.jspecify.annotations.Nullable; - -/** - * A pre-built bundle for a single rule site: the two-element rule-path suffix ({@code - * [FieldRules., Rules.]}), the leaf rule descriptor, and an optional constant - * rule id and message. - * - *

Each native rule type instantiates one {@link RuleSite} per supported rule at class init time - * so violation construction at validation time only allocates the violation builder itself, not the - * path-element protos. Mirrors the {@code ruleSite} struct in protovalidate-go's {@code base.go}. - */ -final class RuleSite { - private final List pathElements; - private final FieldDescriptor leafDescriptor; - private final @Nullable String ruleId; - private final @Nullable String message; - - private RuleSite( - List pathElements, - FieldDescriptor leafDescriptor, - @Nullable String ruleId, - @Nullable String message) { - this.pathElements = pathElements; - this.leafDescriptor = leafDescriptor; - this.ruleId = ruleId; - this.message = message; - } - - /** - * Builds a {@link RuleSite} from a rule-type field descriptor (e.g. {@code FieldRules.string}) - * and a leaf rule field descriptor (e.g. {@code StringRules.min_len}). - * - * @param ruleTypeDescriptor descriptor of the {@code FieldRules} oneof case (e.g. the {@code - * string} field on {@code FieldRules}). - * @param leafDescriptor descriptor of the specific rule (e.g. {@code min_len}). - * @param ruleId optional constant rule id for this site (e.g. {@code "string.min_len"}); may be - * null when the rule id is computed per violation (e.g. well-known formats with empty/error - * variants). - * @param message optional constant violation message; may be null when the message is built per - * violation from the failing value. - */ - static RuleSite of( - FieldDescriptor ruleTypeDescriptor, - FieldDescriptor leafDescriptor, - @Nullable String ruleId, - @Nullable String message) { - List elements = - Collections.unmodifiableList( - Arrays.asList( - FieldPathUtils.fieldPathElement(ruleTypeDescriptor), - FieldPathUtils.fieldPathElement(leafDescriptor))); - return new RuleSite(elements, leafDescriptor, ruleId, message); - } - - List getPathElements() { - return pathElements; - } - - FieldDescriptor getLeafDescriptor() { - return leafDescriptor; - } - - @Nullable String getRuleId() { - return ruleId; - } - - @Nullable String getMessage() { - return message; - } -} diff --git a/src/main/java/build/buf/protovalidate/RuleViolation.java b/src/main/java/build/buf/protovalidate/RuleViolation.java deleted file mode 100644 index 454d2f782..000000000 --- a/src/main/java/build/buf/protovalidate/RuleViolation.java +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import com.google.protobuf.Descriptors; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.List; -import java.util.Objects; -import org.jspecify.annotations.Nullable; - -/** - * {@link RuleViolation} contains all the collected information about an individual rule violation. - */ -final class RuleViolation implements Violation { - /** Static value to return when there are no violations. */ - static final List NO_VIOLATIONS = Collections.emptyList(); - - /** {@link FieldValue} represents a Protobuf field value inside a Protobuf message. */ - static class FieldValue implements Violation.FieldValue { - private final @Nullable Object value; - private final Descriptors.FieldDescriptor descriptor; - - /** - * Constructs a {@link FieldValue} from a value and a descriptor directly. - * - * @param value Bare Protobuf field value of field. - * @param descriptor Field descriptor pertaining to this field. - */ - FieldValue(@Nullable Object value, Descriptors.FieldDescriptor descriptor) { - this.value = value; - this.descriptor = descriptor; - } - - /** - * Constructs a {@link FieldValue} from a {@link Value}. The value must be for a Protobuf field, - * e.g. it must have a FieldDescriptor. - * - * @param value A {@link Value} to create this {@link FieldValue} from. - */ - FieldValue(Value value) { - this.value = value.value(Object.class); - this.descriptor = Objects.requireNonNull(value.fieldDescriptor()); - } - - @Override - public @Nullable Object getValue() { - return value; - } - - @Override - public Descriptors.FieldDescriptor getDescriptor() { - return descriptor; - } - } - - private final build.buf.validate.Violation proto; - private final @Nullable FieldValue fieldValue; - private final @Nullable FieldValue ruleValue; - - /** Builds a Violation instance. */ - static class Builder { - private @Nullable String ruleId; - private @Nullable String message; - private boolean forKey = false; - private final Deque fieldPath = new ArrayDeque<>(); - private final Deque rulePath = new ArrayDeque<>(); - private @Nullable FieldValue fieldValue; - private @Nullable FieldValue ruleValue; - - /** - * Sets the rule ID field of the resulting violation. - * - * @param ruleId Rule ID value to use. - * @return The builder. - */ - Builder setRuleId(String ruleId) { - this.ruleId = ruleId; - return this; - } - - /** - * Sets the message field of the resulting violation. - * - * @param message Message value to use. - * @return The builder. - */ - Builder setMessage(String message) { - this.message = message; - return this; - } - - /** - * Sets whether the violation is for a map key or not. - * - * @param forKey If true, signals that the resulting violation is for a map key. - * @return The builder. - */ - Builder setForKey(boolean forKey) { - this.forKey = forKey; - return this; - } - - /** - * Adds field path elements to the end of the field path. - * - * @param fieldPathElements Field path elements to add. - * @return The builder. - */ - Builder addAllFieldPathElements(Collection fieldPathElements) { - this.fieldPath.addAll(fieldPathElements); - return this; - } - - /** - * Adds a field path element to the beginning of the field path. - * - * @param fieldPathElement A field path element to add to the beginning of the field path. - * @return The builder. - */ - Builder addFirstFieldPathElement(@Nullable FieldPathElement fieldPathElement) { - if (fieldPathElement != null) { - fieldPath.addFirst(fieldPathElement); - } - return this; - } - - /** - * Adds field path elements to the end of the rule path. - * - * @param rulePathElements Field path elements to add. - * @return The builder. - */ - Builder addAllRulePathElements(Collection rulePathElements) { - rulePath.addAll(rulePathElements); - return this; - } - - /** - * Adds a field path element to the beginning of the rule path. - * - * @param rulePathElements A field path element to add to the beginning of the rule path. - * @return The builder. - */ - Builder addFirstRulePathElement(FieldPathElement rulePathElements) { - rulePath.addFirst(rulePathElements); - return this; - } - - /** - * Sets the field value that corresponds to the violation. - * - * @param fieldValue The field value corresponding to this violation. - * @return The builder. - */ - Builder setFieldValue(@Nullable FieldValue fieldValue) { - this.fieldValue = fieldValue; - return this; - } - - /** - * Sets the rule value that corresponds to the violation. - * - * @param ruleValue The rule value corresponding to this violation. - * @return The builder. - */ - Builder setRuleValue(@Nullable FieldValue ruleValue) { - this.ruleValue = ruleValue; - return this; - } - - /** - * Builds a Violation instance with the provided parameters. - * - * @return A Violation instance. - */ - RuleViolation build() { - build.buf.validate.Violation.Builder protoBuilder = build.buf.validate.Violation.newBuilder(); - if (ruleId != null) { - protoBuilder.setRuleId(ruleId); - } - if (message != null) { - protoBuilder.setMessage(message); - } - if (forKey) { - protoBuilder.setForKey(true); - } - if (!fieldPath.isEmpty()) { - protoBuilder.setField(FieldPath.newBuilder().addAllElements(fieldPath)); - } - if (!rulePath.isEmpty()) { - protoBuilder.setRule(FieldPath.newBuilder().addAllElements(rulePath)); - } - return new RuleViolation(protoBuilder.build(), fieldValue, ruleValue); - } - - private Builder() {} - } - - /** - * Creates a new empty builder for building a {@link RuleViolation}. - * - * @return A new, empty {@link Builder}. - */ - static Builder newBuilder() { - return new Builder(); - } - - private RuleViolation( - build.buf.validate.Violation proto, - @Nullable FieldValue fieldValue, - @Nullable FieldValue ruleValue) { - this.proto = proto; - this.fieldValue = fieldValue; - this.ruleValue = ruleValue; - } - - /** - * Gets the protobuf data that corresponds to this rule violation. - * - * @return The protobuf violation data. - */ - @Override - public build.buf.validate.Violation toProto() { - return proto; - } - - /** - * Gets the field value that corresponds to the violation. - * - * @return The field value corresponding to this violation. - */ - @Override - public @Nullable FieldValue getFieldValue() { - return fieldValue; - } - - /** - * Gets the rule value that corresponds to the violation. - * - * @return The rule value corresponding to this violation. - */ - @Override - public @Nullable FieldValue getRuleValue() { - return ruleValue; - } - - @Override - public String toString() { - return proto.toString(); - } -} diff --git a/src/main/java/build/buf/protovalidate/RuleViolationHelper.java b/src/main/java/build/buf/protovalidate/RuleViolationHelper.java deleted file mode 100644 index eaa8b4b8b..000000000 --- a/src/main/java/build/buf/protovalidate/RuleViolationHelper.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import java.util.Collections; -import java.util.List; -import org.jspecify.annotations.Nullable; - -final class RuleViolationHelper { - private static final List EMPTY_PREFIX = Collections.emptyList(); - - private final @Nullable FieldPath rulePrefix; - - private final @Nullable FieldPathElement fieldPathElement; - - RuleViolationHelper(@Nullable ValueEvaluator evaluator) { - if (evaluator != null) { - this.rulePrefix = evaluator.getNestedRule(); - if (evaluator.getDescriptor() != null) { - this.fieldPathElement = FieldPathUtils.fieldPathElement(evaluator.getDescriptor()); - } else { - this.fieldPathElement = null; - } - } else { - this.rulePrefix = null; - this.fieldPathElement = null; - } - } - - @Nullable FieldPathElement getFieldPathElement() { - return fieldPathElement; - } - - List getRulePrefixElements() { - if (rulePrefix == null) { - return EMPTY_PREFIX; - } - return rulePrefix.getElementsList(); - } -} diff --git a/src/main/java/build/buf/protovalidate/Rules.java b/src/main/java/build/buf/protovalidate/Rules.java deleted file mode 100644 index 9f824bb3a..000000000 --- a/src/main/java/build/buf/protovalidate/Rules.java +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldRules; -import com.google.protobuf.Descriptors.FieldDescriptor; -import org.jspecify.annotations.Nullable; - -/** - * Entry point for native rule evaluators. {@code EvaluatorBuilder} calls {@link #tryBuild} once per - * field; if a native evaluator covers some rules, those rules are cleared on the supplied {@code - * FieldRules.Builder} and the residual is then handed to CEL compilation. Returns null when no - * native evaluator applies — CEL handles the field unchanged. - * - *

The clone-and-clear contract ensures forward compatibility: when protovalidate adds a new rule - * that this codebase hasn't yet implemented natively, the rule remains on the residual {@code - * FieldRules} and CEL enforces it. Native rules are an optimization, not a replacement. - */ -final class Rules { - private Rules() {} - - /** - * Attempts to build a native evaluator for the standard rules on {@code fieldDescriptor}. - * - *

Any rule covered natively is cleared on {@code rulesBuilder} so that {@code RuleCache} - * compiles CEL programs only for rules left untouched. The caller is expected to pass a builder - * it owns (typically obtained via {@code fieldRules.toBuilder()} on a clone) and to call {@code - * build()} on the residual before handing it to {@code RuleCache.compile}. - * - * @param fieldDescriptor the field being evaluated - * @param rulesBuilder a mutable builder of the field's {@link FieldRules}; covered rules are - * cleared in place - * @param valueEvaluator the value evaluator the native evaluator will be appended to - * @return a native {@link Evaluator}, or null if no native evaluator applies (CEL handles - * everything) - */ - static @Nullable Evaluator tryBuild( - FieldDescriptor fieldDescriptor, - FieldRules.Builder rulesBuilder, - ValueEvaluator valueEvaluator) { - boolean hasNestedRule = valueEvaluator.hasNestedRule(); - if (fieldDescriptor.isMapField() && !hasNestedRule) { - return MapRulesEvaluator.tryBuild(RuleBase.of(valueEvaluator), rulesBuilder); - } - if (fieldDescriptor.isRepeated() && !hasNestedRule) { - return RepeatedRulesEvaluator.tryBuild(RuleBase.of(valueEvaluator), rulesBuilder); - } - if (!fieldDescriptor.isMapField() && !fieldDescriptor.isRepeated()) { - Evaluator scalar = tryBuildScalarRules(fieldDescriptor, rulesBuilder, valueEvaluator); - if (scalar == null) { - return null; - } - // When processWrapperRules recurses with the inner "value" field, the ValueEvaluator's - // descriptor is still the OUTER wrapper field. Detect that and wrap the scalar evaluator - // so it unwraps the wrapper Message at evaluation time before delegating. - FieldDescriptor outerDescriptor = valueEvaluator.getDescriptor(); - if (outerDescriptor != null - && outerDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { - return new WrappedValueEvaluator(fieldDescriptor, scalar); - } - return scalar; - } - return null; - } - - private static @Nullable Evaluator tryBuildScalarRules( - FieldDescriptor fieldDescriptor, - FieldRules.Builder rulesBuilder, - ValueEvaluator valueEvaluator) { - RuleBase base = RuleBase.of(valueEvaluator); - switch (fieldDescriptor.getJavaType()) { - case BOOLEAN: - return BoolRulesEvaluator.tryBuild(base, rulesBuilder); - case ENUM: - return EnumRulesEvaluator.tryBuild(base, rulesBuilder); - case BYTE_STRING: - return BytesRulesEvaluator.tryBuild(base, rulesBuilder); - case STRING: - return StringRulesEvaluator.tryBuild(base, rulesBuilder); - case INT: - case LONG: - case FLOAT: - case DOUBLE: - NumericTypeConfig config = numericConfigFor(fieldDescriptor); - if (config == null) { - return null; - } - return numericTryBuild(base, rulesBuilder, config); - default: - return null; - } - } - - private static @Nullable NumericTypeConfig numericConfigFor(FieldDescriptor fd) { - switch (fd.getType()) { - case INT32: - return NumericTypeConfig.INT32; - case SINT32: - return NumericTypeConfig.SINT32; - case SFIXED32: - return NumericTypeConfig.SFIXED32; - case UINT32: - return NumericTypeConfig.UINT32; - case FIXED32: - return NumericTypeConfig.FIXED32; - case INT64: - return NumericTypeConfig.INT64; - case SINT64: - return NumericTypeConfig.SINT64; - case SFIXED64: - return NumericTypeConfig.SFIXED64; - case UINT64: - return NumericTypeConfig.UINT64; - case FIXED64: - return NumericTypeConfig.FIXED64; - case FLOAT: - return NumericTypeConfig.FLOAT; - case DOUBLE: - return NumericTypeConfig.DOUBLE; - default: - return null; - } - } - - /** - * Helper that captures the {@code } on {@link NumericTypeConfig} so {@link - * NumericRulesEvaluator#tryBuild} compiles cleanly. The unchecked cast is sound because the - * config's generic parameter is the same as the evaluator's. - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - private static @Nullable Evaluator numericTryBuild( - RuleBase base, FieldRules.Builder rulesBuilder, NumericTypeConfig config) { - return NumericRulesEvaluator.tryBuild(base, rulesBuilder, (NumericTypeConfig) config); - } -} diff --git a/src/main/java/build/buf/protovalidate/StringRulesEvaluator.java b/src/main/java/build/buf/protovalidate/StringRulesEvaluator.java deleted file mode 100644 index 3a5fb6eaa..000000000 --- a/src/main/java/build/buf/protovalidate/StringRulesEvaluator.java +++ /dev/null @@ -1,832 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.validate.FieldRules; -import build.buf.validate.KnownRegex; -import build.buf.validate.StringRules; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.re2j.Pattern; -import java.util.List; -import java.util.Objects; -import org.jspecify.annotations.Nullable; - -/** - * Native evaluator for the standard string rules: scalar rules (`const`, `len`, `min_len`, - * `max_len`, `len_bytes`, `min_bytes`, `max_bytes`, `pattern`, `prefix`, `suffix`, `contains`, - * `not_contains`, `in`, `not_in`), well-known formats (`email`, `hostname`, `ip`, `ipv4`, `ipv6`, - * `uri`, `uri_ref`, `address`, `uuid`, `tuuid`, `ulid`, `host_and_port`, the {@code _prefix} / - * {@code _with_prefixlen} variants), and the {@code well_known_regex} oneof case (HTTP header - * name/value with optional strict mode). - * - *

Mirrors {@code nativeStringEval} in protovalidate-go's {@code native_string.go}. Format - * helpers from {@link CustomOverload} are reused so native and CEL paths share the same - * email/hostname/IP/URI parsers. - */ -final class StringRulesEvaluator implements Evaluator { - - // --- Static descriptors and rule sites --- - - private static final FieldDescriptor STRING_RULES_DESC = - FieldRules.getDescriptor().findFieldByNumber(FieldRules.STRING_FIELD_NUMBER); - - private static final RuleSite CONST_SITE = site(StringRules.CONST_FIELD_NUMBER, "string.const"); - private static final RuleSite LEN_SITE = site(StringRules.LEN_FIELD_NUMBER, "string.len"); - private static final RuleSite MIN_LEN_SITE = - site(StringRules.MIN_LEN_FIELD_NUMBER, "string.min_len"); - private static final RuleSite MAX_LEN_SITE = - site(StringRules.MAX_LEN_FIELD_NUMBER, "string.max_len"); - private static final RuleSite LEN_BYTES_SITE = - site(StringRules.LEN_BYTES_FIELD_NUMBER, "string.len_bytes"); - private static final RuleSite MIN_BYTES_SITE = - site(StringRules.MIN_BYTES_FIELD_NUMBER, "string.min_bytes"); - private static final RuleSite MAX_BYTES_SITE = - site(StringRules.MAX_BYTES_FIELD_NUMBER, "string.max_bytes"); - private static final RuleSite PATTERN_SITE = - site(StringRules.PATTERN_FIELD_NUMBER, "string.pattern"); - private static final RuleSite PREFIX_SITE = - site(StringRules.PREFIX_FIELD_NUMBER, "string.prefix"); - private static final RuleSite SUFFIX_SITE = - site(StringRules.SUFFIX_FIELD_NUMBER, "string.suffix"); - private static final RuleSite CONTAINS_SITE = - site(StringRules.CONTAINS_FIELD_NUMBER, "string.contains"); - private static final RuleSite NOT_CONTAINS_SITE = - site(StringRules.NOT_CONTAINS_FIELD_NUMBER, "string.not_contains"); - private static final RuleSite IN_SITE = site(StringRules.IN_FIELD_NUMBER, "string.in"); - private static final RuleSite NOT_IN_SITE = - site(StringRules.NOT_IN_FIELD_NUMBER, "string.not_in"); - private static final FieldDescriptor WELL_KNOWN_REGEX_DESC = - StringRules.getDescriptor().findFieldByNumber(StringRules.WELL_KNOWN_REGEX_FIELD_NUMBER); - - // well_known_regex sites: pre-built once. Header-name and header-value have a normal-failure - // site and an empty-input site (header_name); pattern selection at evaluation time picks - // between strict and loose matchers. - private static final RuleSite HEADER_NAME_SITE = - RuleSite.of( - STRING_RULES_DESC, - WELL_KNOWN_REGEX_DESC, - "string.well_known_regex.header_name", - "must be a valid HTTP header name"); - private static final RuleSite HEADER_NAME_EMPTY_SITE = - RuleSite.of( - STRING_RULES_DESC, - WELL_KNOWN_REGEX_DESC, - "string.well_known_regex.header_name_empty", - "value is empty, which is not a valid HTTP header name"); - private static final RuleSite HEADER_VALUE_SITE = - RuleSite.of( - STRING_RULES_DESC, - WELL_KNOWN_REGEX_DESC, - "string.well_known_regex.header_value", - "must be a valid HTTP header value"); - - private static RuleSite site(int fieldNumber, String ruleId) { - FieldDescriptor leaf = StringRules.getDescriptor().findFieldByNumber(fieldNumber); - return RuleSite.of(STRING_RULES_DESC, leaf, ruleId, null); - } - - // --- Static regexes (compile once) --- - - private static final Pattern UUID_REGEX = - Pattern.compile( - "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); - private static final Pattern TUUID_REGEX = Pattern.compile("^[0-9a-fA-F]{32}$"); - private static final Pattern ULID_REGEX = - Pattern.compile("^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$"); - private static final Pattern HEADER_NAME_REGEX = - Pattern.compile("^:?[0-9a-zA-Z!#$%&'*+.\\-^_|~`]+$"); - private static final Pattern HEADER_VALUE_REGEX = - Pattern.compile("^[^\\x00-\\x08\\x0A-\\x1F\\x7F]*$"); - private static final Pattern LOOSE_REGEX = Pattern.compile("^[^\\x00\\x0A\\x0D]+$"); - - // --- Well-known string formats --- - - /** - * Each constant carries the rule id, the main violation message, the empty-value variant message, - * and the validation. Empty messages are stored verbatim from the proto spec rather than derived - * by string substitution: {@code host_and_port}, for example, has a main message of {@code "must - * be a valid host (hostname or IP address) and port pair"} but an empty message of {@code "value - * is empty, which is not a valid host and port pair"} (without the parenthetical) — substring - * derivation produced the wrong text. - */ - @SuppressWarnings("ImmutableEnumChecker") // RuleSite is logically immutable; not annotated. - enum WellKnownFormat { - EMAIL( - StringRules.EMAIL_FIELD_NUMBER, - "email", - "must be a valid email address", - "value is empty, which is not a valid email address") { - @Override - boolean validate(String s) { - return CustomOverload.isEmail(s); - } - }, - HOSTNAME( - StringRules.HOSTNAME_FIELD_NUMBER, - "hostname", - "must be a valid hostname", - "value is empty, which is not a valid hostname") { - @Override - boolean validate(String s) { - return CustomOverload.isHostname(s); - } - }, - IP( - StringRules.IP_FIELD_NUMBER, - "ip", - "must be a valid IP address", - "value is empty, which is not a valid IP address") { - @Override - boolean validate(String s) { - return CustomOverload.isIp(s, 0); - } - }, - IPV4( - StringRules.IPV4_FIELD_NUMBER, - "ipv4", - "must be a valid IPv4 address", - "value is empty, which is not a valid IPv4 address") { - @Override - boolean validate(String s) { - return CustomOverload.isIp(s, 4); - } - }, - IPV6( - StringRules.IPV6_FIELD_NUMBER, - "ipv6", - "must be a valid IPv6 address", - "value is empty, which is not a valid IPv6 address") { - @Override - boolean validate(String s) { - return CustomOverload.isIp(s, 6); - } - }, - URI( - StringRules.URI_FIELD_NUMBER, - "uri", - "must be a valid URI", - "value is empty, which is not a valid URI") { - @Override - boolean validate(String s) { - return CustomOverload.isUri(s); - } - }, - URI_REF(StringRules.URI_REF_FIELD_NUMBER, "uri_ref", "must be a valid URI Reference", null) { - @Override - boolean validate(String s) { - return CustomOverload.isUriRef(s); - } - - @Override - boolean checksEmpty() { - return false; - } - }, - ADDRESS( - StringRules.ADDRESS_FIELD_NUMBER, - "address", - "must be a valid hostname, or ip address", - "value is empty, which is not a valid hostname, or ip address") { - @Override - boolean validate(String s) { - return CustomOverload.isHostname(s) || CustomOverload.isIp(s, 0); - } - }, - UUID( - StringRules.UUID_FIELD_NUMBER, - "uuid", - "must be a valid UUID", - "value is empty, which is not a valid UUID") { - @Override - boolean validate(String s) { - return UUID_REGEX.matches(s); - } - }, - TUUID( - StringRules.TUUID_FIELD_NUMBER, - "tuuid", - "must be a valid trimmed UUID", - "value is empty, which is not a valid trimmed UUID") { - @Override - boolean validate(String s) { - return TUUID_REGEX.matches(s); - } - }, - IP_WITH_PREFIXLEN( - StringRules.IP_WITH_PREFIXLEN_FIELD_NUMBER, - "ip_with_prefixlen", - "must be a valid IP prefix", - "value is empty, which is not a valid IP prefix") { - @Override - boolean validate(String s) { - return CustomOverload.isIpPrefix(s, 0, false); - } - }, - IPV4_WITH_PREFIXLEN( - StringRules.IPV4_WITH_PREFIXLEN_FIELD_NUMBER, - "ipv4_with_prefixlen", - "must be a valid IPv4 address with prefix length", - "value is empty, which is not a valid IPv4 address with prefix length") { - @Override - boolean validate(String s) { - return CustomOverload.isIpPrefix(s, 4, false); - } - }, - IPV6_WITH_PREFIXLEN( - StringRules.IPV6_WITH_PREFIXLEN_FIELD_NUMBER, - "ipv6_with_prefixlen", - "must be a valid IPv6 address with prefix length", - "value is empty, which is not a valid IPv6 address with prefix length") { - @Override - boolean validate(String s) { - return CustomOverload.isIpPrefix(s, 6, false); - } - }, - IP_PREFIX( - StringRules.IP_PREFIX_FIELD_NUMBER, - "ip_prefix", - "must be a valid IP prefix", - "value is empty, which is not a valid IP prefix") { - @Override - boolean validate(String s) { - return CustomOverload.isIpPrefix(s, 0, true); - } - }, - IPV4_PREFIX( - StringRules.IPV4_PREFIX_FIELD_NUMBER, - "ipv4_prefix", - "must be a valid IPv4 prefix", - "value is empty, which is not a valid IPv4 prefix") { - @Override - boolean validate(String s) { - return CustomOverload.isIpPrefix(s, 4, true); - } - }, - IPV6_PREFIX( - StringRules.IPV6_PREFIX_FIELD_NUMBER, - "ipv6_prefix", - "must be a valid IPv6 prefix", - "value is empty, which is not a valid IPv6 prefix") { - @Override - boolean validate(String s) { - return CustomOverload.isIpPrefix(s, 6, true); - } - }, - HOST_AND_PORT( - StringRules.HOST_AND_PORT_FIELD_NUMBER, - "host_and_port", - "must be a valid host (hostname or IP address) and port pair", - "value is empty, which is not a valid host and port pair") { - @Override - boolean validate(String s) { - return CustomOverload.isHostAndPort(s, true); - } - }, - ULID( - StringRules.ULID_FIELD_NUMBER, - "ulid", - "must be a valid ULID", - "value is empty, which is not a valid ULID") { - @Override - boolean validate(String s) { - return ULID_REGEX.matches(s); - } - }; - - final FieldDescriptor field; - final RuleSite site; - final @Nullable RuleSite emptySite; - - WellKnownFormat( - int fieldNumber, String ruleSuffix, String message, @Nullable String emptyMessage) { - FieldDescriptor leaf = StringRules.getDescriptor().findFieldByNumber(fieldNumber); - this.field = leaf; - this.site = RuleSite.of(STRING_RULES_DESC, leaf, "string." + ruleSuffix, message); - this.emptySite = - emptyMessage == null - ? null - : RuleSite.of( - STRING_RULES_DESC, leaf, "string." + ruleSuffix + "_empty", emptyMessage); - } - - /** Whether this format reports an empty-value violation distinctly from the format failure. */ - boolean checksEmpty() { - return true; - } - - abstract boolean validate(String s); - } - - // --- Fields --- - - private final RuleBase base; - private final @Nullable String constVal; - private final @Nullable Long exactLen; - private final @Nullable Long minLen; - private final @Nullable Long maxLen; - private final @Nullable Long exactBytes; - private final @Nullable Long minBytes; - private final @Nullable Long maxBytes; - private final @Nullable Pattern pattern; - private final @Nullable String patternStr; - private final @Nullable String prefix; - private final @Nullable String suffix; - private final @Nullable String contains; - private final @Nullable String notContains; - private final List inVals; - private final List notInVals; - private final @Nullable WellKnownFormat wellKnown; - private final KnownRegex knownRegex; - private final boolean knownRegexStrict; - - private StringRulesEvaluator( - RuleBase base, - @Nullable String constVal, - @Nullable Long exactLen, - @Nullable Long minLen, - @Nullable Long maxLen, - @Nullable Long exactBytes, - @Nullable Long minBytes, - @Nullable Long maxBytes, - @Nullable Pattern pattern, - @Nullable String patternStr, - @Nullable String prefix, - @Nullable String suffix, - @Nullable String contains, - @Nullable String notContains, - List inVals, - List notInVals, - @Nullable WellKnownFormat wellKnown, - KnownRegex knownRegex, - boolean knownRegexStrict) { - this.base = base; - this.constVal = constVal; - this.exactLen = exactLen; - this.minLen = minLen; - this.maxLen = maxLen; - this.exactBytes = exactBytes; - this.minBytes = minBytes; - this.maxBytes = maxBytes; - this.pattern = pattern; - this.patternStr = patternStr; - this.prefix = prefix; - this.suffix = suffix; - this.contains = contains; - this.notContains = notContains; - this.inVals = inVals; - this.notInVals = notInVals; - this.wellKnown = wellKnown; - this.knownRegex = knownRegex; - this.knownRegexStrict = knownRegexStrict; - } - - static @Nullable Evaluator tryBuild(RuleBase base, FieldRules.Builder rulesBuilder) { - if (!rulesBuilder.hasString()) { - return null; - } - StringRules rules = rulesBuilder.getString(); - if (!rules.getUnknownFields().isEmpty()) { - return null; - } - - StringRules.Builder sb = rules.toBuilder(); - boolean hasRule = false; - - // Well-known oneof: at most one of the format fields, OR well_known_regex, can be set. - WellKnownFormat wellKnown = null; - KnownRegex knownRegex = KnownRegex.KNOWN_REGEX_UNSPECIFIED; - boolean knownRegexStrict = false; - for (WellKnownFormat fmt : WellKnownFormat.values()) { - if (rules.hasField(fmt.field)) { - boolean enabled = (Boolean) rules.getField(fmt.field); - if (enabled) { - wellKnown = fmt; - sb.clearField(fmt.field); - hasRule = true; - } - break; - } - } - if (wellKnown == null && rules.hasWellKnownRegex()) { - knownRegex = rules.getWellKnownRegex(); - // strict defaults to true when not explicitly set. - knownRegexStrict = !rules.hasStrict() || rules.getStrict(); - if (knownRegex != KnownRegex.KNOWN_REGEX_UNSPECIFIED) { - sb.clearWellKnownRegex(); - if (rules.hasStrict()) { - sb.clearStrict(); - } - hasRule = true; - } - } - - String constVal = null; - if (rules.hasConst()) { - constVal = rules.getConst(); - sb.clearConst(); - hasRule = true; - } - Long exactLen = null; - if (rules.hasLen()) { - exactLen = rules.getLen(); - sb.clearLen(); - hasRule = true; - } - Long minLen = null; - if (rules.hasMinLen()) { - minLen = rules.getMinLen(); - sb.clearMinLen(); - hasRule = true; - } - Long maxLen = null; - if (rules.hasMaxLen()) { - maxLen = rules.getMaxLen(); - sb.clearMaxLen(); - hasRule = true; - } - Long exactBytes = null; - if (rules.hasLenBytes()) { - exactBytes = rules.getLenBytes(); - sb.clearLenBytes(); - hasRule = true; - } - Long minBytes = null; - if (rules.hasMinBytes()) { - minBytes = rules.getMinBytes(); - sb.clearMinBytes(); - hasRule = true; - } - Long maxBytes = null; - if (rules.hasMaxBytes()) { - maxBytes = rules.getMaxBytes(); - sb.clearMaxBytes(); - hasRule = true; - } - - Pattern compiledPattern = null; - String patternStr = null; - if (rules.hasPattern()) { - patternStr = rules.getPattern(); - try { - compiledPattern = Pattern.compile(patternStr); - } catch (com.google.re2j.PatternSyntaxException e) { - return null; // bail to CEL — same compilation error - } - sb.clearPattern(); - hasRule = true; - } - - String prefix = null; - if (rules.hasPrefix()) { - prefix = rules.getPrefix(); - sb.clearPrefix(); - hasRule = true; - } - String suffix = null; - if (rules.hasSuffix()) { - suffix = rules.getSuffix(); - sb.clearSuffix(); - hasRule = true; - } - String contains = null; - if (rules.hasContains()) { - contains = rules.getContains(); - sb.clearContains(); - hasRule = true; - } - String notContains = null; - if (rules.hasNotContains()) { - notContains = rules.getNotContains(); - sb.clearNotContains(); - hasRule = true; - } - - // getInList()/getNotInList() return immutable views from the proto runtime; we only read - // them, so no defensive copy is needed. - List inVals = rules.getInList(); - if (!inVals.isEmpty()) { - sb.clearIn(); - hasRule = true; - } - - List notInVals = rules.getNotInList(); - if (!notInVals.isEmpty()) { - sb.clearNotIn(); - hasRule = true; - } - - if (!hasRule) { - return null; - } - rulesBuilder.setString(sb.build()); - return new StringRulesEvaluator( - base, - constVal, - exactLen, - minLen, - maxLen, - exactBytes, - minBytes, - maxBytes, - compiledPattern, - patternStr, - prefix, - suffix, - contains, - notContains, - inVals, - notInVals, - wellKnown, - knownRegex, - knownRegexStrict); - } - - @Override - public boolean tautology() { - return false; - } - - @Override - public List evaluate(Value val, boolean failFast) { - String strVal = (String) val.rawValue(); - List violations = null; - - if (exactLen != null || minLen != null || maxLen != null) { - long runeCount = strVal.codePointCount(0, strVal.length()); - violations = applyLength(violations, val, runeCount, failFast); - if (failFast && violations != null) { - return base.done(violations); - } - } - - if (exactBytes != null || minBytes != null || maxBytes != null) { - long byteCount = utf8ByteLength(strVal); - violations = applyByteLength(violations, val, byteCount, failFast); - if (failFast && violations != null) { - return base.done(violations); - } - } - - if (constVal != null && !strVal.equals(constVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - CONST_SITE, null, "must equal `" + constVal + "`", val, constVal)); - if (failFast) return base.done(violations); - } - - if (pattern != null && !pattern.matches(strVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - PATTERN_SITE, - null, - "does not match regex pattern `" + patternStr + "`", - val, - patternStr)); - if (failFast) return base.done(violations); - } - - if (prefix != null && !strVal.startsWith(prefix)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - PREFIX_SITE, null, "does not have prefix `" + prefix + "`", val, prefix)); - if (failFast) return base.done(violations); - } - - if (suffix != null && !strVal.endsWith(suffix)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - SUFFIX_SITE, null, "does not have suffix `" + suffix + "`", val, suffix)); - if (failFast) return base.done(violations); - } - - if (contains != null && !strVal.contains(contains)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - CONTAINS_SITE, - null, - "does not contain substring `" + contains + "`", - val, - contains)); - if (failFast) return base.done(violations); - } - - if (notContains != null && strVal.contains(notContains)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - NOT_CONTAINS_SITE, - null, - "contains substring `" + notContains + "`", - val, - notContains)); - if (failFast) return base.done(violations); - } - - if (!inVals.isEmpty() && !inVals.contains(strVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - IN_SITE, null, "must be in list " + RuleBase.formatList(inVals), val, inVals)); - if (failFast) return base.done(violations); - } - - if (!notInVals.isEmpty() && notInVals.contains(strVal)) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - NOT_IN_SITE, - null, - "must not be in list " + RuleBase.formatList(notInVals), - val, - notInVals)); - if (failFast) return base.done(violations); - } - - if (wellKnown != null) { - RuleViolation.Builder wkv = checkWellKnown(strVal, val); - if (wkv != null) { - violations = RuleBase.add(violations, wkv); - if (failFast) return base.done(violations); - } - } else if (knownRegex != KnownRegex.KNOWN_REGEX_UNSPECIFIED) { - RuleViolation.Builder krv = checkKnownRegex(strVal, val); - if (krv != null) { - violations = RuleBase.add(violations, krv); - if (failFast) return base.done(violations); - } - } - - return base.done(violations); - } - - // --- Length checks --- - - private @Nullable List applyLength( - @Nullable List violations, - Value val, - long runeCount, - boolean failFast) { - if (exactLen != null && runeCount != exactLen) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - LEN_SITE, null, "must be " + exactLen + " characters", val, exactLen)); - if (failFast) return violations; - } - if (minLen != null && runeCount < minLen) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MIN_LEN_SITE, null, "must be at least " + minLen + " characters", val, minLen)); - if (failFast) return violations; - } - if (maxLen != null && runeCount > maxLen) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MAX_LEN_SITE, null, "must be at most " + maxLen + " characters", val, maxLen)); - if (failFast) return violations; - } - return violations; - } - - private @Nullable List applyByteLength( - @Nullable List violations, - Value val, - long byteCount, - boolean failFast) { - if (exactBytes != null && byteCount != exactBytes) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - LEN_BYTES_SITE, null, "must be " + exactBytes + " bytes", val, exactBytes)); - if (failFast) return violations; - } - if (minBytes != null && byteCount < minBytes) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MIN_BYTES_SITE, null, "must be at least " + minBytes + " bytes", val, minBytes)); - if (failFast) return violations; - } - if (maxBytes != null && byteCount > maxBytes) { - violations = - RuleBase.add( - violations, - NativeViolations.newViolation( - MAX_BYTES_SITE, null, "must be at most " + maxBytes + " bytes", val, maxBytes)); - if (failFast) return violations; - } - return violations; - } - - // --- Well-known format check --- - - private RuleViolation.@Nullable Builder checkWellKnown(String strVal, Value val) { - WellKnownFormat fmt = wellKnown; - if (fmt == null) { - return null; - } - if (fmt.checksEmpty() && strVal.isEmpty()) { - // checksEmpty() returning true implies emptySite is non-null (the WellKnownFormat - // constructor's contract). - RuleSite emptySite = Objects.requireNonNull(fmt.emptySite); - return NativeViolations.newViolation(emptySite, null, null, val, true); - } - if (fmt.validate(strVal)) { - return null; - } - return NativeViolations.newViolation(fmt.site, null, null, val, true); - } - - private RuleViolation.@Nullable Builder checkKnownRegex(String strVal, Value val) { - Pattern matcher; - RuleSite site; - switch (knownRegex) { - case KNOWN_REGEX_HTTP_HEADER_NAME: - if (strVal.isEmpty()) { - return NativeViolations.newViolation( - HEADER_NAME_EMPTY_SITE, null, null, val, knownRegex.getNumber()); - } - matcher = HEADER_NAME_REGEX; - site = HEADER_NAME_SITE; - break; - case KNOWN_REGEX_HTTP_HEADER_VALUE: - matcher = HEADER_VALUE_REGEX; - site = HEADER_VALUE_SITE; - break; - default: - return null; - } - if (!knownRegexStrict) { - matcher = LOOSE_REGEX; - } - if (!matcher.matches(strVal)) { - return NativeViolations.newViolation(site, null, null, val, knownRegex.getNumber()); - } - return null; - } - - // --- Helpers --- - - /** - * Returns the number of bytes in the UTF-8 encoding of {@code s} without allocating the byte - * array. Counts each code point's encoded byte length: 1 for U+0000–U+007F, 2 for U+0080–U+07FF, - * 3 for U+0800–U+FFFF (excluding the surrogate range, which is consumed as a pair), 4 for - * supplementary characters (U+10000–U+10FFFF). - */ - private static long utf8ByteLength(String s) { - long count = 0; - int i = 0; - int len = s.length(); - while (i < len) { - char c = s.charAt(i); - if (c < 0x80) { - count += 1; - i += 1; - } else if (c < 0x800) { - count += 2; - i += 1; - } else if (Character.isHighSurrogate(c) - && i + 1 < len - && Character.isLowSurrogate(s.charAt(i + 1))) { - count += 4; - i += 2; - } else { - count += 3; - i += 1; - } - } - return count; - } -} diff --git a/src/main/java/build/buf/protovalidate/Uri.java b/src/main/java/build/buf/protovalidate/Uri.java deleted file mode 100644 index 5977b7f2b..000000000 --- a/src/main/java/build/buf/protovalidate/Uri.java +++ /dev/null @@ -1,941 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CoderResult; -import java.nio.charset.StandardCharsets; - -/** Ipv6 is a class used to parse a given string to determine if it is a URI or URI reference. */ -final class Uri { - private final String str; - private int index; - private boolean pctEncodedFound; - - Uri(String str) { - this.str = str; - } - - /** - * Determines whether string is a valid URI. - * - *

Parses the rule: - * - *

URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
-   */
-  boolean uri() {
-    int start = this.index;
-
-    if (!(this.scheme() && this.take(':') && this.hierPart())) {
-      this.index = start;
-      return false;
-    }
-
-    if (this.take('?') && !this.query()) {
-      return false;
-    }
-
-    if (this.take('#') && !this.fragment()) {
-      return false;
-    }
-
-    if (this.index != this.str.length()) {
-      this.index = start;
-      return false;
-    }
-
-    return true;
-  }
-
-  /**
-   * Determines whether the current position is a valid hier-part.
-   *
-   * 

Parses the rule: - * - *

hier-part = "//" authority path-abempty
-   *                / path-absolute
-   *                / path-rootless
-   *                / path-empty
-   */
-  private boolean hierPart() {
-    int start = this.index;
-
-    if (this.takeDoubleSlash() && this.authority() && this.pathAbempty()) {
-      return true;
-    }
-
-    this.index = start;
-
-    return this.pathAbsolute() || this.pathRootless() || this.pathEmpty();
-  }
-
-  /**
-   * Determines whether string is a valid URI reference.
-   *
-   * 

Parses the rule: - * - *

URI-reference = URI / relative-ref
-   */
-  boolean uriReference() {
-    return this.uri() || this.relativeRef();
-  }
-
-  /**
-   * Determines whether the current position is a valid relative reference.
-   *
-   * 

Parses the rule: - * - *

relative-ref = relative-part [ "?" query ] [ "#" fragment ].
-   */
-  private boolean relativeRef() {
-    int start = this.index;
-
-    if (!this.relativePart()) {
-      return false;
-    }
-
-    if (this.take('?') && !this.query()) {
-      this.index = start;
-      return false;
-    }
-
-    if (this.take('#') && !this.fragment()) {
-      this.index = start;
-      return false;
-    }
-
-    if (this.index != this.str.length()) {
-      this.index = start;
-      return false;
-    }
-
-    return true;
-  }
-
-  /**
-   * Determines whether the current position is a valid relative part.
-   *
-   * 

Parses the rule: - * - *

relative-part = "//" authority path-abempty
-   *                    / path-absolute
-   *                    / path-noscheme
-   *                    / path-empty
-   */
-  private boolean relativePart() {
-    int start = this.index;
-
-    if (this.takeDoubleSlash() && this.authority() && this.pathAbempty()) {
-      return true;
-    }
-
-    this.index = start;
-
-    return this.pathAbsolute() || this.pathNoscheme() || this.pathEmpty();
-  }
-
-  private boolean takeDoubleSlash() {
-    boolean isSlash = take('/');
-
-    return isSlash && take('/');
-  }
-
-  /**
-   * Determines whether the current position is a valid scheme.
-   *
-   * 

Parses the rule: - * - *

scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
-   */
-  private boolean scheme() {
-    int start = this.index;
-
-    if (this.alpha()) {
-      while (this.alpha() || this.digit() || this.take('+') || this.take('-') || this.take('.')) {}
-
-      if (this.peek(':')) {
-        return true;
-      }
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid authority.
-   *
-   * 

Parses the rule: - * - *

authority = [ userinfo "@" ] host [ ":" port ]
-   *
-   * Lead by double slash ("") and terminated by "/", "?", "#", or end of URI.
-   */
-  private boolean authority() {
-    int start = this.index;
-
-    if (this.userinfo()) {
-      if (!this.take('@')) {
-        this.index = start;
-        return false;
-      }
-    }
-
-    if (!this.host()) {
-      this.index = start;
-      return false;
-    }
-
-    if (this.take(':')) {
-      if (!this.port()) {
-        this.index = start;
-        return false;
-      }
-    }
-
-    if (!this.isAuthorityEnd()) {
-      this.index = start;
-      return false;
-    }
-
-    return true;
-  }
-
-  /**
-   * Determines whether the current position is the end of the authority.
-   *
-   * 

The authority component [...] is terminated by one of the following: - * - *

    - *
  • the next slash ("/") - *
  • question mark ("?") - *
  • number sign ("#") character - *
  • the end of the URI. - *
- */ - private boolean isAuthorityEnd() { - if (this.index >= this.str.length()) { - return true; - } - char c = this.str.charAt(this.index); - return (c == '?' || c == '#' || c == '/'); - } - - /** - * Determines whether the current position is a valid userinfo. - * - *

Parses the rule: - * - *

userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
-   *
-   * Terminated by "@" in authority.
-   */
-  private boolean userinfo() {
-    int start = this.index;
-
-    while (true) {
-      if (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take(':')) {
-        continue;
-      }
-
-      if (this.peek('@')) {
-        return true;
-      }
-
-      this.index = start;
-
-      return false;
-    }
-  }
-
-  private static int unhex(char c) {
-    if ('0' <= c && c <= '9') {
-      return c - '0';
-    } else if ('a' <= c && c <= 'f') {
-      return c - 'a' + 10;
-    } else if ('A' <= c && c <= 'F') {
-      return c - 'A' + 10;
-    }
-
-    return 0;
-  }
-
-  /**
-   * Verifies that str is correctly percent-encoded.
-   *
-   * 

Note that we essentially want to mimic the behavior of decodeURIComponent, which would fail - * on malformed URLs. Java does have various methods for decoding URLs, but none behave - * consistently with decodeURIComponent. - * - *

The code below is a combination of `checkHostPctEncoded` from the protovalidate-go - * implementation and Java's java.net.URI#decode methods. - */ - private boolean checkHostPctEncoded(String str) { - CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); - - int strLen = str.length(); - ByteBuffer buffer = ByteBuffer.allocate(strLen); - CharBuffer out = CharBuffer.allocate(strLen); - - // Unhex str and convert to a ByteBuffer. - for (int i = 0; i < str.length(); ) { - if (str.charAt(i) == '%') { - // If we encounter a %, unhex the two following digits, extract their - // last 4 bits, cast to a byte. - byte b = - (byte) - (((unhex(str.charAt(i + 1)) & 0xf) << 4) | ((unhex(str.charAt(i + 2)) & 0xf) << 0)); - buffer.put(b); - i += 3; - } else { - // Not percent encoded, extract the last 4 bits, convert to a byte - // and add to the byte buffer. - buffer.put((byte) (str.charAt(i) & 0xf)); - i++; - } - } - - // Attempt to decode the byte buffer as UTF-8. - CoderResult f = decoder.decode((ByteBuffer) buffer.flip(), out, true); - - // If an error occurred, return false as invalid. - if (f.isError()) { - return false; - } - // Flush the buffer - f = decoder.flush(out); - - // If an error occurred, return false as invalid. - // Otherwise return true. - return !f.isError(); - } - - /** - * Determines whether the current position is a valid host. - * - *

Parses the rule: - * - *

host = IP-literal / IPv4address / reg-name.
-   */
-  private boolean host() {
-    int start = this.index;
-    this.pctEncodedFound = false;
-
-    // Note: IPv4address is a subset of reg-name
-    if ((this.peek('[') && this.ipLiteral()) || this.regName()) {
-      if (this.pctEncodedFound) {
-        String rawHost = this.str.substring(start, this.index);
-        // RFC 3986:
-        // > URI producing applications must not use percent-encoding in host
-        // > unless it is used to represent a UTF-8 character sequence.
-        return this.checkHostPctEncoded(rawHost);
-      }
-
-      return true;
-    }
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid port.
-   *
-   * 

Parses the rule: - * - *

port = *DIGIT
-   *
-   * Terminated by end of authority.
-   */
-  private boolean port() {
-    int start = this.index;
-
-    while (true) {
-      if (this.digit()) {
-        continue;
-      }
-
-      if (this.isAuthorityEnd()) {
-        return true;
-      }
-
-      this.index = start;
-
-      return false;
-    }
-  }
-
-  /**
-   * Determines whether the current position is a valid IP literal.
-   *
-   * 

Parses the rule from RFC 6874: - * - *

IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture  ) "]"
-   */
-  private boolean ipLiteral() {
-    int start = this.index;
-
-    if (this.take('[')) {
-      int j = this.index;
-
-      if (this.ipv6Address() && this.take(']')) {
-        return true;
-      }
-
-      this.index = j;
-
-      if (this.ipv6Addrz() && this.take(']')) {
-        return true;
-      }
-
-      this.index = j;
-
-      if (this.ipvFuture() && this.take(']')) {
-        return true;
-      }
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid ipv6 address.
-   *
-   * 

ipv6Address parses the rule "IPv6address". - * - *

Relies on the implementation of isIp. - */ - private boolean ipv6Address() { - int start = this.index; - - while (this.hexDig() || this.take(':')) {} - - if (CustomOverload.isIp(this.str.substring(start, this.index), 6)) { - return true; - } - - this.index = start; - - return false; - } - - /** - * Determines whether the current position is a valid IPv6addrz. - * - *

Parses the rule: - * - *

IPv6addrz = IPv6address "%25" ZoneID
-   */
-  private boolean ipv6Addrz() {
-    int start = this.index;
-
-    if (this.ipv6Address() && this.take('%') && this.take('2') && this.take('5') && this.zoneID()) {
-      return true;
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid zone ID.
-   *
-   * 

Parses the rule: - * - *

ZoneID = 1*( unreserved / pct-encoded )
-   */
-  private boolean zoneID() {
-    int start = this.index;
-
-    while (this.unreserved() || this.pctEncoded()) {}
-
-    if (this.index - start > 0) {
-      return true;
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid IPvFuture.
-   *
-   * 

Parses the rule: - * - *

IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
-   */
-  private boolean ipvFuture() {
-    int start = this.index;
-
-    if (this.take('v') && this.hexDig()) {
-      while (this.hexDig()) {}
-
-      if (this.take('.')) {
-        int j = 0;
-
-        while (this.unreserved() || this.subDelims() || this.take(':')) {
-          j++;
-        }
-
-        if (j >= 1) {
-          return true;
-        }
-      }
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid reg-name.
-   *
-   * 

Parses the rule: - * - *

reg-name = *( unreserved / pct-encoded / sub-delims )
-   *
-   * Terminates on start of port (":") or end of authority.
-   */
-  private boolean regName() {
-    int start = this.index;
-
-    while (true) {
-      if (this.unreserved() || this.pctEncoded() || this.subDelims()) {
-        continue;
-      }
-
-      if (this.isAuthorityEnd()) {
-        // End of authority
-        return true;
-      }
-
-      if (this.peek(':')) {
-        return true;
-      }
-
-      this.index = start;
-
-      return false;
-    }
-  }
-
-  /**
-   * Determines whether the current position is the end of the path.
-   *
-   * 

The path is terminated by one of the following: - * - *

    - *
  • the first question mark ("?") - *
  • number sign ("#") character - *
  • the end of the URI. - *
- */ - private boolean isPathEnd() { - if (this.index >= this.str.length()) { - return true; - } - - char c = this.str.charAt(this.index); - - return (c == '?' || c == '#'); - } - - /** - * Determines whether the current position is a valid path-abempty. - * - *

Parses the rule: - * - *

path-abempty = *( "/" segment )
-   *
-   * Terminated by end of path: "?", "#", or end of URI.
-   */
-  private boolean pathAbempty() {
-    int start = this.index;
-
-    while (this.take('/') && this.segment()) {}
-
-    if (this.isPathEnd()) {
-      return true;
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid path-absolute.
-   *
-   * 

Parses the rule: - * - *

path-absolute = "/" [ segment-nz *( "/" segment ) ]
-   *
-   * Terminated by end of path: "?", "#", or end of URI.
-   */
-  private boolean pathAbsolute() {
-    int start = this.index;
-
-    if (this.take('/')) {
-      if (this.segmentNz()) {
-        while (this.take('/') && this.segment()) {}
-      }
-
-      if (this.isPathEnd()) {
-        return true;
-      }
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid path-noscheme.
-   *
-   * 

Parses the rule: - * - *

path-noscheme = segment-nz-nc *( "/" segment )
-   *
-   * Terminated by end of path: "?", "#", or end of URI.
-   */
-  private boolean pathNoscheme() {
-    int start = this.index;
-
-    if (this.segmentNzNc()) {
-      while (this.take('/') && this.segment()) {}
-
-      if (this.isPathEnd()) {
-        return true;
-      }
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid path-rootless.
-   *
-   * 

Parses the rule: - * - *

path-rootless = segment-nz *( "/" segment )
-   *
-   * Terminated by end of path: "?", "#", or end of URI.
-   */
-  private boolean pathRootless() {
-    int start = this.index;
-
-    if (this.segmentNz()) {
-      while (this.take('/') && this.segment()) {}
-
-      if (this.isPathEnd()) {
-        return true;
-      }
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid path-empty.
-   *
-   * 

Parses the rule: - * - *

path-empty = 0
-   *
-   * Terminated by end of path: "?", "#", or end of URI.
-   */
-  private boolean pathEmpty() {
-    return this.isPathEnd();
-  }
-
-  /**
-   * Determines whether the current position is a valid segment.
-   *
-   * 

Parses the rule: - * - *

segment = *pchar
-   */
-  private boolean segment() {
-    while (this.pchar()) {}
-
-    return true;
-  }
-
-  /**
-   * Determines whether the current position is a valid segment-nz.
-   *
-   * 

Parses the rule: - * - *

segment-nz = 1*pchar
-   */
-  private boolean segmentNz() {
-    int start = this.index;
-
-    if (this.pchar()) {
-      while (this.pchar()) {}
-      return true;
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid segment-nz-nc.
-   *
-   * 

Parses the rule: - * - *

segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
-   *                   ; non-zero-length segment without any colon ":"
-   */
-  private boolean segmentNzNc() {
-    int start = this.index;
-
-    while (this.unreserved() || this.pctEncoded() || this.subDelims() || this.take('@')) {}
-
-    if (this.index - start > 0) {
-      return true;
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a valid pchar.
-   *
-   * 

Parses the rule: - * - *

pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
-   */
-  private boolean pchar() {
-    return (this.unreserved()
-        || this.pctEncoded()
-        || this.subDelims()
-        || this.take(':')
-        || this.take('@'));
-  }
-
-  /**
-   * Determines whether the current position is a valid query.
-   *
-   * 

Parses the rule: - * - *

query = *( pchar / "/" / "?" )
-   *
-   * Terminated by "#" or end of URI.
-   */
-  private boolean query() {
-    int start = this.index;
-
-    while (true) {
-      if (this.pchar() || this.take('/') || this.take('?')) {
-        continue;
-      }
-
-      if (this.peek('#') || this.index == this.str.length()) {
-        return true;
-      }
-
-      this.index = start;
-
-      return false;
-    }
-  }
-
-  /**
-   * Determines whether the current position is a valid fragment.
-   *
-   * 

Parses the rule: - * - *

fragment = *( pchar / "/" / "?" )
-   *
-   * Terminated by end of URI.
-   */
-  private boolean fragment() {
-    int start = this.index;
-
-    while (true) {
-      if (this.pchar() || this.take('/') || this.take('?')) {
-        continue;
-      }
-
-      if (this.index == this.str.length()) {
-        return true;
-      }
-
-      this.index = start;
-
-      return false;
-    }
-  }
-
-  /**
-   * Determines whether the current position is a valid pct-encoded.
-   *
-   * 

Parses the rule: - * - *

pct-encoded = "%"+HEXDIG+HEXDIG
-   *
-   * Sets `pctEncodedFound` to true if a valid triplet was found.
-   */
-  private boolean pctEncoded() {
-    int start = this.index;
-
-    if (this.take('%') && this.hexDig() && this.hexDig()) {
-      this.pctEncodedFound = true;
-
-      return true;
-    }
-
-    this.index = start;
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is an unreserved character.
-   *
-   * 

Parses the rule: - * - *

unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
-   */
-  private boolean unreserved() {
-    return (this.alpha()
-        || this.digit()
-        || this.take('-')
-        || this.take('_')
-        || this.take('.')
-        || this.take('~'));
-  }
-
-  /**
-   * Determines whether the current position is a sub-delim.
-   *
-   * 

Parses the rule: - * - *

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
-   *                  / "*" / "+" / "," / ";" / "="
-   */
-  private boolean subDelims() {
-    return (this.take('!')
-        || this.take('$')
-        || this.take('&')
-        || this.take('\'')
-        || this.take('(')
-        || this.take(')')
-        || this.take('*')
-        || this.take('+')
-        || this.take(',')
-        || this.take(';')
-        || this.take('='));
-  }
-
-  /**
-   * Determines whether the current position is an alpha character.
-   *
-   * 

Parses the rule: - * - *

ALPHA =  %x41-5A / %x61-7A ; A-Z / a-z
-   */
-  private boolean alpha() {
-    if (this.index >= this.str.length()) {
-      return false;
-    }
-
-    char c = this.str.charAt(this.index);
-
-    if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
-      this.index++;
-      return true;
-    }
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a hex digit.
-   *
-   * 

Parses the rule: - * - *

HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
-   */
-  private boolean hexDig() {
-    if (this.index >= this.str.length()) {
-      return false;
-    }
-
-    char c = this.str.charAt(this.index);
-
-    if (('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')) {
-      this.index++;
-      return true;
-    }
-
-    return false;
-  }
-
-  /**
-   * Determines whether the current position is a digit.
-   *
-   * 

Parses the rule: - * - *

DIGIT = %x30-39 ; 0-9
-   */
-  private boolean digit() {
-    if (this.index >= this.str.length()) {
-      return false;
-    }
-
-    char c = this.str.charAt(this.index);
-    if ('0' <= c && c <= '9') {
-      this.index++;
-      return true;
-    }
-    return false;
-  }
-
-  /** Take the given char at the current position, incrementing the index if necessary. */
-  private boolean take(char c) {
-    if (this.index >= this.str.length()) {
-      return false;
-    }
-
-    if (this.str.charAt(this.index) == c) {
-      this.index++;
-      return true;
-    }
-
-    return false;
-  }
-
-  private boolean peek(char c) {
-    return this.index < this.str.length() && this.str.charAt(this.index) == c;
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/ValidateLibrary.java b/src/main/java/build/buf/protovalidate/ValidateLibrary.java
deleted file mode 100644
index 9078e8356..000000000
--- a/src/main/java/build/buf/protovalidate/ValidateLibrary.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import com.google.re2j.Pattern;
-import dev.cel.bundle.Cel;
-import dev.cel.bundle.CelFactory;
-import dev.cel.checker.CelCheckerBuilder;
-import dev.cel.checker.CelCheckerLegacyImpl;
-import dev.cel.checker.CelStandardDeclarations;
-import dev.cel.common.CelOptions;
-import dev.cel.common.CelVarDecl;
-import dev.cel.common.types.SimpleType;
-import dev.cel.compiler.CelCompiler;
-import dev.cel.compiler.CelCompilerImpl;
-import dev.cel.compiler.CelCompilerLibrary;
-import dev.cel.extensions.CelExtensions;
-import dev.cel.parser.CelParserBuilder;
-import dev.cel.parser.CelParserImpl;
-import dev.cel.parser.CelStandardMacro;
-import dev.cel.runtime.CelRuntime;
-import dev.cel.runtime.CelRuntimeBuilder;
-import dev.cel.runtime.CelRuntimeImpl;
-import dev.cel.runtime.CelRuntimeLibrary;
-import dev.cel.runtime.CelStandardFunctions;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-/**
- * Custom {@link CelCompilerLibrary} and {@link CelRuntimeLibrary}. Provides all the custom
- * extension function definitions and overloads.
- */
-final class ValidateLibrary implements CelCompilerLibrary, CelRuntimeLibrary {
-
-  private static final CelOptions CEL_OPTIONS =
-      CelOptions.current().enableHeterogeneousNumericComparisons(true).build();
-
-  private final ConcurrentMap patternCache = new ConcurrentHashMap<>();
-
-  /** Creates a ValidateLibrary with all custom declarations and overloads. */
-  ValidateLibrary() {}
-
-  static Cel newCel() {
-    ValidateLibrary validateLibrary = new ValidateLibrary();
-    // Wired by hand instead of via plannerCelBuilder(): CelRuntimeImpl directs callers to subset
-    // stdlib via setStandardFunctions rather than setStandardEnvironmentEnabled, so the runtime
-    // does that while the checker uses setStandardEnvironmentEnabled(false).
-    CelCompiler compiler =
-        CelCompilerImpl.newBuilder(
-                CelParserImpl.newBuilder(),
-                CelCheckerLegacyImpl.newBuilder().setStandardEnvironmentEnabled(false))
-            .setOptions(CEL_OPTIONS)
-            // Drop stdlib matches; CustomOverload provides a caching replacement.
-            // Ref: https://github.com/google/cel-java/issues/1038
-            .setStandardDeclarations(
-                CelStandardDeclarations.newBuilder()
-                    .excludeFunctions(CelStandardDeclarations.StandardFunction.MATCHES)
-                    .build())
-            .addLibraries(validateLibrary, CelExtensions.strings())
-            .build();
-    CelRuntime runtime =
-        CelRuntimeImpl.newBuilder()
-            .setOptions(CEL_OPTIONS)
-            .setStandardFunctions(
-                CelStandardFunctions.newBuilder()
-                    .excludeFunctions(CelStandardFunctions.StandardFunction.MATCHES)
-                    .build())
-            .addLibraries(validateLibrary, CelExtensions.strings())
-            .build();
-    return CelFactory.combine(compiler, runtime);
-  }
-
-  @Override
-  public void setParserOptions(CelParserBuilder parserBuilder) {
-    parserBuilder.setStandardMacros(
-        CelStandardMacro.ALL,
-        CelStandardMacro.EXISTS,
-        CelStandardMacro.EXISTS_ONE,
-        CelStandardMacro.FILTER,
-        CelStandardMacro.HAS,
-        CelStandardMacro.MAP,
-        CelStandardMacro.MAP_FILTER);
-  }
-
-  @Override
-  public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
-    checkerBuilder
-        .addVarDeclarations(
-            CelVarDecl.newVarDeclaration(NowVariable.NOW_NAME, SimpleType.TIMESTAMP))
-        .addFunctionDeclarations(CustomDeclarations.create());
-  }
-
-  @Override
-  public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
-    runtimeBuilder.addFunctionBindings(CustomOverload.create(patternCache, CEL_OPTIONS));
-  }
-}
diff --git a/src/main/java/build/buf/protovalidate/ValidationResult.java b/src/main/java/build/buf/protovalidate/ValidationResult.java
index 5373b5a8a..3fb08e92e 100644
--- a/src/main/java/build/buf/protovalidate/ValidationResult.java
+++ b/src/main/java/build/buf/protovalidate/ValidationResult.java
@@ -1,4 +1,4 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
+// Copyright 2023 Buf Technologies, Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
 
 package build.buf.protovalidate;
 
-import build.buf.validate.Violations;
-import java.util.ArrayList;
+import build.buf.gen.buf.validate.Violation;
 import java.util.Collections;
 import java.util.List;
 
 /**
- * {@link ValidationResult} is returned when a rule is executed. It contains a list of violations.
- * This is non-fatal. If there are no violations, the rule is considered to have passed.
+ * {@link ValidationResult} is returned when a constraint is executed. It contains a list of
+ * violations. This is non-fatal. If there are no violations, the constraint is considered to have
+ * passed.
  */
 public class ValidationResult {
 
@@ -48,7 +48,7 @@ public ValidationResult(List violations) {
    * @return if the validation result was a success.
    */
   public boolean isSuccess() {
-    return violations.isEmpty();
+    return !violations.isEmpty();
   }
 
   /**
@@ -68,34 +68,15 @@ public List getViolations() {
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
-    if (isSuccess()) {
-      builder.append("Validation OK");
-    } else {
-      builder.append("Validation error:");
-      for (Violation violation : violations) {
-        builder.append("\n - ");
-        if (violation.toProto().hasField()) {
-          builder.append(FieldPathUtils.fieldPathString(violation.toProto().getField()));
-          builder.append(": ");
-        }
-        builder.append(
-            String.format(
-                "%s [%s]", violation.toProto().getMessage(), violation.toProto().getRuleId()));
+    builder.append("Validation error:");
+    for (Violation violation : violations) {
+      builder.append("\n - ");
+      if (!violation.getFieldPath().isEmpty()) {
+        builder.append(violation.getFieldPath());
+        builder.append(": ");
       }
+      builder.append(String.format("%s [%s]", violation.getMessage(), violation.getConstraintId()));
     }
     return builder.toString();
   }
-
-  /**
-   * Converts the validation result to its equivalent protobuf form.
-   *
-   * @return The protobuf form of this validation result.
-   */
-  public build.buf.validate.Violations toProto() {
-    List protoViolations = new ArrayList<>();
-    for (Violation violation : violations) {
-      protoViolations.add(violation.toProto());
-    }
-    return Violations.newBuilder().addAllViolations(protoViolations).build();
-  }
 }
diff --git a/src/main/java/build/buf/protovalidate/Validator.java b/src/main/java/build/buf/protovalidate/Validator.java
index 449abef73..5683bc84d 100644
--- a/src/main/java/build/buf/protovalidate/Validator.java
+++ b/src/main/java/build/buf/protovalidate/Validator.java
@@ -1,4 +1,4 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
+// Copyright 2023 Buf Technologies, Inc.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,24 +15,92 @@
 package build.buf.protovalidate;
 
 import build.buf.protovalidate.exceptions.CompilationException;
-import build.buf.protovalidate.exceptions.ExecutionException;
 import build.buf.protovalidate.exceptions.ValidationException;
+import build.buf.protovalidate.internal.celext.ValidateLibrary;
+import build.buf.protovalidate.internal.evaluator.Evaluator;
+import build.buf.protovalidate.internal.evaluator.EvaluatorBuilder;
+import build.buf.protovalidate.internal.evaluator.MessageValue;
+import com.google.protobuf.Descriptors.Descriptor;
 import com.google.protobuf.Message;
+import org.projectnessie.cel.Env;
+import org.projectnessie.cel.Library;
+
+/** Performs validation on any proto.Message values. The Validator is safe for concurrent use. */
+public class Validator {
+  /** evaluatorBuilder is the builder used to construct the evaluator for a given message. */
+  private final EvaluatorBuilder evaluatorBuilder;
+
+  /**
+   * failFast indicates whether the validator should stop evaluating constraints after the first
+   * violation.
+   */
+  private final boolean failFast;
+
+  /**
+   * Constructs a new {@link Validator}.
+   *
+   * @param config specified configuration.
+   */
+  public Validator(Config config) {
+    Env env = Env.newEnv(Library.Lib(new ValidateLibrary()));
+    this.evaluatorBuilder = new EvaluatorBuilder(env, config.isDisableLazy());
+    this.failFast = config.isFailFast();
+  }
+
+  /** Constructs a new {@link Validator} with a default configuration. */
+  public Validator() {
+    Config config = Config.builder().build();
+    Env env = Env.newEnv(Library.Lib(new ValidateLibrary()));
+    this.evaluatorBuilder = new EvaluatorBuilder(env, config.isDisableLazy());
+    this.failFast = config.isFailFast();
+  }
 
-/** A validator that can be used to validate messages */
-public interface Validator {
   /**
-   * Checks that message satisfies its rules. Rules are defined within the Protobuf file as options
-   * from the buf.validate package. A {@link ValidationResult} is returned which contains a list of
-   * violations. If the list is empty, the message is valid. If the list is non-empty, the message
-   * is invalid. An exception is thrown if the message cannot be validated because the evaluation
-   * logic for the message cannot be built ({@link CompilationException}), or there is a type error
-   * when attempting to evaluate a CEL expression associated with the message ({@link
-   * ExecutionException}).
+   * Checks that message satisfies its constraints. Constraints are defined within the Protobuf file
+   * as options from the buf.validate package. A {@link ValidationResult} is returned which contains
+   * a list of violations. If the list is empty, the message is valid. If the list is non-empty, the
+   * message is invalid. An exception is thrown if the message cannot be validated because the
+   * evaluation logic for the message cannot be built ({@link CompilationException}), or there is a
+   * type error when attempting to evaluate a CEL expression associated with the message ({@link
+   * build.buf.protovalidate.exceptions.ExecutionException}).
    *
    * @param msg the {@link Message} to be validated.
    * @return the {@link ValidationResult} from the evaluation.
    * @throws ValidationException if there are any compilation or validation execution errors.
    */
-  ValidationResult validate(Message msg) throws ValidationException;
+  public ValidationResult validate(Message msg) throws ValidationException {
+    if (msg == null) {
+      return ValidationResult.EMPTY;
+    }
+    Descriptor descriptor = msg.getDescriptorForType();
+    Evaluator evaluator = evaluatorBuilder.load(descriptor);
+    return evaluator.evaluate(new MessageValue(msg), failFast);
+  }
+
+  /**
+   * Loads messages that are expected to be validated, allowing the {@link Validator} to warm up.
+   * Messages included transitively (i.e., fields with message values) are automatically handled.
+   *
+   * @param messages the list of {@link Message} to load.
+   * @throws CompilationException if there are any compilation errors during warm-up.
+   */
+  public void loadMessages(Message... messages) throws CompilationException {
+    for (Message message : messages) {
+      this.evaluatorBuilder.load(message.getDescriptorForType());
+    }
+  }
+
+  /**
+   * Loads message descriptors that are expected to be validated, allowing the {@link Validator} to
+   * warm up. Messages included transitively (i.e., fields with message values) are automatically
+   * handled.
+   *
+   * @param descriptors the list of {@link Descriptor} to load.
+   * @throws CompilationException if there are any compilation errors during warm-up.
+   */
+  public void loadDescriptors(Descriptor... descriptors) throws CompilationException {
+    for (Descriptor descriptor : descriptors) {
+      this.evaluatorBuilder.load(descriptor);
+    }
+  }
 }
diff --git a/src/main/java/build/buf/protovalidate/ValidatorFactory.java b/src/main/java/build/buf/protovalidate/ValidatorFactory.java
deleted file mode 100644
index 78a807274..000000000
--- a/src/main/java/build/buf/protovalidate/ValidatorFactory.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright 2023-2026 Buf Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package build.buf.protovalidate;
-
-import build.buf.protovalidate.exceptions.CompilationException;
-import com.google.protobuf.Descriptors.Descriptor;
-import java.util.List;
-import org.jspecify.annotations.Nullable;
-
-/**
- * ValidatorFactory is used to create a validator.
- *
- * 

Validators can be created with an optional {@link Config} to customize behavior. They can also - * be created with a list of seed descriptors to warmup the validator cache ahead of time as well as - * an indicator to lazily-load any descriptors not provided into the cache. - */ -public final class ValidatorFactory { - // Prevent instantiation - private ValidatorFactory() {} - - /** A builder class used for building a validator. */ - public static class ValidatorBuilder { - /** The config object to use for instantiating a validator. */ - @Nullable private Config config; - - /** - * Create a validator with the given config - * - * @param config The {@link Config} to configure the validator. - * @return The builder instance - */ - public ValidatorBuilder withConfig(Config config) { - this.config = config; - return this; - } - - // Prevent instantiation - private ValidatorBuilder() {} - - /** - * Build a new validator - * - * @return A new {@link Validator} instance. - */ - public Validator build() { - Config cfg = this.config; - if (cfg == null) { - cfg = Config.newBuilder().build(); - } - return new ValidatorImpl(cfg); - } - - /** - * Build the validator, warming up the cache with any provided descriptors. - * - * @param descriptors the list of descriptors to warm up the cache. - * @param disableLazy whether to disable lazy loading of validation rules. When validation is - * performed, a message's rules will be looked up in a cache. If they are not found, by - * default they will be processed and lazily-loaded into the cache. Setting this to false - * will not attempt to lazily-load descriptor information not found in the cache and - * essentially makes the entire cache read-only, eliminating thread contention. - * @return A new {@link Validator} instance. - * @throws CompilationException If any of the given descriptors' validation rules fail - * processing while warming up the cache. - * @throws IllegalStateException If disableLazy is set to true and no descriptors are passed. - */ - public Validator buildWithDescriptors(List descriptors, boolean disableLazy) - throws CompilationException, IllegalStateException { - if (disableLazy && (descriptors == null || descriptors.isEmpty())) { - throw new IllegalStateException( - "a list of descriptors is required when disableLazy is true"); - } - - Config cfg = this.config; - if (cfg == null) { - cfg = Config.newBuilder().build(); - } - return new ValidatorImpl(cfg, descriptors, disableLazy); - } - } - - /** - * Creates a new builder for a validator. - * - * @return A Validator builder - */ - public static ValidatorBuilder newBuilder() { - return new ValidatorBuilder(); - } -} diff --git a/src/main/java/build/buf/protovalidate/ValidatorImpl.java b/src/main/java/build/buf/protovalidate/ValidatorImpl.java deleted file mode 100644 index 39613c9f8..000000000 --- a/src/main/java/build/buf/protovalidate/ValidatorImpl.java +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.CompilationException; -import build.buf.protovalidate.exceptions.ValidationException; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.Message; -import java.util.ArrayList; -import java.util.List; - -final class ValidatorImpl implements Validator { - /** evaluatorBuilder is the builder used to construct the evaluator for a given message. */ - private final EvaluatorBuilder evaluatorBuilder; - - /** - * failFast indicates whether the validator should stop evaluating rules after the first - * violation. - */ - private final boolean failFast; - - ValidatorImpl(Config config) { - this.evaluatorBuilder = new EvaluatorBuilder(ValidateLibrary.newCel(), config); - this.failFast = config.isFailFast(); - } - - ValidatorImpl(Config config, List descriptors, boolean disableLazy) - throws CompilationException { - this.evaluatorBuilder = - new EvaluatorBuilder(ValidateLibrary.newCel(), config, descriptors, disableLazy); - this.failFast = config.isFailFast(); - } - - @Override - public ValidationResult validate(Message msg) throws ValidationException { - if (msg == null) { - return ValidationResult.EMPTY; - } - Descriptor descriptor = msg.getDescriptorForType(); - Evaluator evaluator = evaluatorBuilder.load(descriptor); - List result = evaluator.evaluate(new MessageValue(msg), this.failFast); - if (result.isEmpty()) { - return ValidationResult.EMPTY; - } - List violations = new ArrayList<>(result.size()); - for (RuleViolation.Builder builder : result) { - violations.add(builder.build()); - } - return new ValidationResult(violations); - } -} diff --git a/src/main/java/build/buf/protovalidate/ValueEvaluator.java b/src/main/java/build/buf/protovalidate/ValueEvaluator.java deleted file mode 100644 index 433da1c40..000000000 --- a/src/main/java/build/buf/protovalidate/ValueEvaluator.java +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.FieldPath; -import com.google.protobuf.Descriptors; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import org.jspecify.annotations.Nullable; - -/** - * {@link ValueEvaluator} performs validation on any concrete value contained within a singular - * field, repeated elements, or the keys/values of a map. - */ -final class ValueEvaluator implements Evaluator { - /** The {@link Descriptors.FieldDescriptor} targeted by this evaluator */ - private final Descriptors.@Nullable FieldDescriptor descriptor; - - /** The nested rule path that this value evaluator is for */ - @Nullable private final FieldPath nestedRule; - - /** The default or zero-value for this value's type. */ - @Nullable private Object zero; - - /** The evaluators applied to a value. */ - private final List evaluators = new ArrayList<>(); - - /** - * Indicates that the Rules should not be applied if the field is unset or the default (typically - * zero) value. - */ - private boolean ignoreEmpty; - - /** Constructs a {@link ValueEvaluator}. */ - ValueEvaluator(Descriptors.@Nullable FieldDescriptor descriptor, @Nullable FieldPath nestedRule) { - this.descriptor = descriptor; - this.nestedRule = nestedRule; - } - - Descriptors.@Nullable FieldDescriptor getDescriptor() { - return descriptor; - } - - @Nullable FieldPath getNestedRule() { - return nestedRule; - } - - boolean hasNestedRule() { - return this.nestedRule != null; - } - - @Override - public boolean tautology() { - return evaluators.isEmpty(); - } - - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - if (this.shouldIgnore(val.value(Object.class))) { - return RuleViolation.NO_VIOLATIONS; - } - List allViolations = new ArrayList<>(); - for (Evaluator evaluator : evaluators) { - List violations = evaluator.evaluate(val, failFast); - if (failFast && !violations.isEmpty()) { - return violations; - } - allViolations.addAll(violations); - } - if (allViolations.isEmpty()) { - return RuleViolation.NO_VIOLATIONS; - } - return allViolations; - } - - /** - * Appends an evaluator to the list of evaluators. - * - * @param eval The evaluator to append. - */ - void append(Evaluator eval) { - if (!eval.tautology()) { - this.evaluators.add(eval); - } - } - - void setIgnoreEmpty(Object zero) { - this.ignoreEmpty = true; - this.zero = zero; - } - - private boolean shouldIgnore(Object value) { - return this.ignoreEmpty && Objects.equals(value, this.zero); - } -} diff --git a/src/main/java/build/buf/protovalidate/Variable.java b/src/main/java/build/buf/protovalidate/Variable.java deleted file mode 100644 index 592d20328..000000000 --- a/src/main/java/build/buf/protovalidate/Variable.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import dev.cel.runtime.CelVariableResolver; -import java.util.Optional; -import org.jspecify.annotations.Nullable; - -/** - * {@link Variable} implements {@link CelVariableResolver}, providing a lightweight named variable - * to cel.Program executions. - */ -final class Variable implements CelVariableResolver { - /** The {@value} variable in CEL. */ - static final String THIS_NAME = "this"; - - /** The {@value} variable in CEL. */ - static final String RULES_NAME = "rules"; - - /** The {@value} variable in CEL. */ - static final String RULE_NAME = "rule"; - - /** The variable's name */ - private final String name; - - /** The value for this variable */ - @Nullable private final Object val; - - /** Creates a variable with the given name and value. */ - private Variable(String name, @Nullable Object val) { - this.name = name; - this.val = val; - } - - /** - * Creates a "this" variable. - * - * @param val the value. - * @return {@link Variable}. - */ - static CelVariableResolver newThisVariable(@Nullable Object val) { - return CelVariableResolver.hierarchicalVariableResolver( - new NowVariable(), new Variable(THIS_NAME, val)); - } - - /** - * Creates a "rules" variable. - * - * @param val the value. - * @return {@link Variable}. - */ - static CelVariableResolver newRulesVariable(Object val) { - return new Variable(RULES_NAME, val); - } - - /** - * Creates a "rule" variable. - * - * @param rules the value of the "rules" variable. - * @param val the value of the "rule" variable. - * @return {@link Variable}. - */ - static CelVariableResolver newRuleVariable(Object rules, Object val) { - return CelVariableResolver.hierarchicalVariableResolver( - newRulesVariable(rules), new Variable(RULE_NAME, val)); - } - - @Override - public Optional find(String name) { - if (!this.name.equals(name) || val == null) { - return Optional.empty(); - } - return Optional.of(val); - } -} diff --git a/src/main/java/build/buf/protovalidate/Violation.java b/src/main/java/build/buf/protovalidate/Violation.java deleted file mode 100644 index 85e7b946d..000000000 --- a/src/main/java/build/buf/protovalidate/Violation.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import com.google.protobuf.Descriptors; -import org.jspecify.annotations.Nullable; - -/** {@link Violation} provides all the collected information about an individual rule violation. */ -public interface Violation { - /** {@link FieldValue} represents a Protobuf field value inside a Protobuf message. */ - interface FieldValue { - /** - * Gets the value of the field, which may be null, a primitive, a Map or a List. - * - * @return The value of the protobuf field. - */ - @Nullable Object getValue(); - - /** - * Gets the field descriptor of the field this value is from. - * - * @return A FieldDescriptor pertaining to this field. - */ - Descriptors.FieldDescriptor getDescriptor(); - } - - /** - * Gets the protobuf form of this violation. - * - * @return The protobuf form of this violation. - */ - build.buf.validate.Violation toProto(); - - /** - * Gets the value of the field this violation pertains to, or null if there is none. - * - * @return Value of the field associated with the violation, or null if there is none. - */ - @Nullable FieldValue getFieldValue(); - - /** - * Gets the value of the rule this violation pertains to, or null if there is none. - * - * @return Value of the rule associated with the violation, or null if there is none. - */ - @Nullable FieldValue getRuleValue(); -} diff --git a/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java b/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java deleted file mode 100644 index 6cba9a242..000000000 --- a/src/main/java/build/buf/protovalidate/WrappedValueEvaluator.java +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import build.buf.protovalidate.exceptions.ExecutionException; -import com.google.protobuf.Descriptors.FieldDescriptor; -import com.google.protobuf.Message; -import java.util.List; - -/** - * Adapter that lets a scalar-rule evaluator run against a {@code google.protobuf.*Value} wrapper - * field. At evaluation time it pulls the inner {@code value} field off the wrapper {@link Message} - * and delegates to the wrapped scalar evaluator. - * - *

CEL's runtime auto-unwraps wrappers to their inner primitive type, so the CEL path doesn't - * need this adapter. Native evaluators expect the underlying scalar (e.g. {@code Long} for {@code - * Int64Value.value}); without unwrapping they'd see the wrapper {@link Message} and misbehave. - * Mirrors {@code wrappedValueEval} in protovalidate-go's {@code builder.go}. - * - *

The wrapped evaluator's {@link RuleBase} is constructed against the OUTER wrapper field's - * {@code ValueEvaluator}, so violation field paths point at the user's wrapper-typed field rather - * than the synthetic inner {@code value}. - */ -final class WrappedValueEvaluator implements Evaluator { - private final FieldDescriptor innerField; - private final Evaluator inner; - - WrappedValueEvaluator(FieldDescriptor innerField, Evaluator inner) { - // innerField must be the synthetic "value" field of a google.protobuf.*Value wrapper. Its - // containing message holds exactly one field at number 1 named "value"; if any of those - // assumptions is violated the evaluator would silently misbehave at runtime. - if (innerField.getNumber() != 1 || !"value".equals(innerField.getName())) { - throw new IllegalArgumentException( - "WrappedValueEvaluator requires the wrapper's inner 'value' field, got " - + innerField.getFullName()); - } - this.innerField = innerField; - this.inner = inner; - } - - @Override - public boolean tautology() { - return inner.tautology(); - } - - @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - Message message = val.messageValue(); - if (message == null) { - // proto3 message-typed field absent — no value to validate. - return RuleViolation.NO_VIOLATIONS; - } - Object innerValue = message.getField(innerField); - return inner.evaluate(new ObjectValue(innerField, innerValue), failFast); - } -} diff --git a/src/main/java/build/buf/protovalidate/exceptions/CompilationException.java b/src/main/java/build/buf/protovalidate/exceptions/CompilationException.java index 00337cafd..72e59ea5d 100644 --- a/src/main/java/build/buf/protovalidate/exceptions/CompilationException.java +++ b/src/main/java/build/buf/protovalidate/exceptions/CompilationException.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,24 +14,12 @@ package build.buf.protovalidate.exceptions; -/** CompilationException is returned when a rule fails to compile. This is a fatal error. */ +/** + * {@link CompilationException} extends {@link ValidationException} is returned when a constraint + * fails to compile. This is a fatal error. + */ public class CompilationException extends ValidationException { - /** - * Creates a CompilationException with the specified message. - * - * @param message Exception message. - */ public CompilationException(String message) { super(message); } - - /** - * Creates a CompilationException with the specified message and cause. - * - * @param message Exception message. - * @param cause Underlying cause of the exception. - */ - public CompilationException(String message, Throwable cause) { - super(message, cause); - } } diff --git a/src/main/java/build/buf/protovalidate/exceptions/ExecutionException.java b/src/main/java/build/buf/protovalidate/exceptions/ExecutionException.java index 041279fdb..7c56fddb8 100644 --- a/src/main/java/build/buf/protovalidate/exceptions/ExecutionException.java +++ b/src/main/java/build/buf/protovalidate/exceptions/ExecutionException.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,13 +14,11 @@ package build.buf.protovalidate.exceptions; -/** ExecutionException is returned when a rule fails to execute. This is a fatal error. */ +/** + * {@link ExecutionException} extends {@link ValidationException} is returned when a constraint + * fails to execute. This is a fatal error. + */ public class ExecutionException extends ValidationException { - /** - * Creates an ExecutionException with the specified message. - * - * @param message Exception message. - */ public ExecutionException(String message) { super(message); } diff --git a/src/main/java/build/buf/protovalidate/exceptions/ValidationException.java b/src/main/java/build/buf/protovalidate/exceptions/ValidationException.java index 6d3a4e097..e5069e750 100644 --- a/src/main/java/build/buf/protovalidate/exceptions/ValidationException.java +++ b/src/main/java/build/buf/protovalidate/exceptions/ValidationException.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,24 +14,9 @@ package build.buf.protovalidate.exceptions; -/** ValidationException is the base exception for all validation errors. */ +/** Extends {@link Exception} is the base exception for all validation errors. */ public class ValidationException extends Exception { - /** - * Creates a ValidationException with the specified message. - * - * @param message Exception message. - */ public ValidationException(String message) { super(message); } - - /** - * Creates a ValidationException with the specified message and cause. - * - * @param message Exception message. - * @param cause Underlying cause of the exception. - */ - public ValidationException(String message, Throwable cause) { - super(message, cause); - } } diff --git a/src/main/java/build/buf/protovalidate/internal/celext/CustomDeclarations.java b/src/main/java/build/buf/protovalidate/internal/celext/CustomDeclarations.java new file mode 100644 index 000000000..3906b49f4 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/celext/CustomDeclarations.java @@ -0,0 +1,169 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.celext; + +import com.google.api.expr.v1alpha1.Decl; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.TimestampT; + +/** Defines custom declaration functions. */ +final class CustomDeclarations { + + /** + * Create the custom function declaration list. + * + * @return the list of function declarations. + */ + static List create() { + List decls = new ArrayList<>(); + + // Add 'now' variable declaration + decls.add(Decls.newVar("now", Decls.newObjectType(TimestampT.TimestampType.typeName()))); + + // Add 'isIp' function declaration + decls.add( + Decls.newFunction( + "isIp", + Decls.newInstanceOverload("is_ip", Arrays.asList(Decls.String, Decls.Int), Decls.Bool), + Decls.newInstanceOverload( + "is_ip_unary", Collections.singletonList(Decls.String), Decls.Bool))); + + // Add 'isUriRef' function declaration + decls.add( + Decls.newFunction( + "isUriRef", + Decls.newInstanceOverload( + "is_uri_ref", Collections.singletonList(Decls.String), Decls.Bool))); + + // Add 'isUri' function declaration + decls.add( + Decls.newFunction( + "isUri", + Decls.newInstanceOverload( + "is_uri", Collections.singletonList(Decls.String), Decls.Bool))); + + // Add 'isEmail' function declaration + decls.add( + Decls.newFunction( + "isEmail", + Decls.newInstanceOverload( + "is_email", Collections.singletonList(Decls.String), Decls.Bool))); + + // Add 'isHostname' function declaration + decls.add( + Decls.newFunction( + "isHostname", + Decls.newInstanceOverload( + "is_hostname", Collections.singletonList(Decls.String), Decls.Bool))); + + // Add 'startsWith' function declaration + decls.add( + Decls.newFunction( + "startsWith", + Decls.newInstanceOverload( + "starts_with_bytes", Arrays.asList(Decls.Bytes, Decls.Bytes), Decls.Bool))); + + // Add 'endsWith' function declaration + decls.add( + Decls.newFunction( + "endsWith", + Decls.newInstanceOverload( + "ends_with_bytes", Arrays.asList(Decls.Bytes, Decls.Bytes), Decls.Bool))); + + // Add 'contains' function declaration + decls.add( + Decls.newFunction( + "contains", + Decls.newInstanceOverload( + "contains_bytes", Arrays.asList(Decls.Bytes, Decls.Bytes), Decls.Bool))); + + // Add 'unique' function declaration + List uniqueOverloads = new ArrayList<>(); + for (com.google.api.expr.v1alpha1.Type type : + Arrays.asList(Decls.String, Decls.Int, Decls.Uint, Decls.Double, Decls.Bytes, Decls.Bool)) { + uniqueOverloads.add( + Decls.newInstanceOverload( + String.format( + "unique_%s", + org.projectnessie.cel.checker.Types.formatCheckedType(type) + .toLowerCase(Locale.US)), + Collections.singletonList(type), + Decls.Bool)); + uniqueOverloads.add( + Decls.newInstanceOverload( + String.format( + "unique_list_%s", + org.projectnessie.cel.checker.Types.formatCheckedType(type) + .toLowerCase(Locale.US)), + Collections.singletonList(Decls.newListType(type)), + Decls.Bool)); + } + decls.add(Decls.newFunction("unique", uniqueOverloads)); + + // Add 'format' function declaration + List formatOverloads = new ArrayList<>(); + for (com.google.api.expr.v1alpha1.Type type : + Arrays.asList( + Decls.String, + Decls.Int, + Decls.Uint, + Decls.Double, + Decls.Bytes, + Decls.Bool, + Decls.Duration, + Decls.Timestamp)) { + formatOverloads.add( + Decls.newInstanceOverload( + String.format( + "format_%s", + org.projectnessie.cel.checker.Types.formatCheckedType(type) + .toLowerCase(Locale.US)), + Arrays.asList(Decls.String, Decls.newListType(type)), + Decls.String)); + formatOverloads.add( + Decls.newInstanceOverload( + String.format( + "format_list_%s", + org.projectnessie.cel.checker.Types.formatCheckedType(type) + .toLowerCase(Locale.US)), + Arrays.asList(Decls.String, Decls.newListType(Decls.newListType(type))), + Decls.String)); + formatOverloads.add( + Decls.newInstanceOverload( + String.format( + "format_bytes_%s", + org.projectnessie.cel.checker.Types.formatCheckedType(type) + .toLowerCase(Locale.US)), + Arrays.asList(Decls.Bytes, Decls.newListType(type)), + Decls.Bytes)); + formatOverloads.add( + Decls.newInstanceOverload( + String.format( + "format_bytes_list_%s", + org.projectnessie.cel.checker.Types.formatCheckedType(type) + .toLowerCase(Locale.US)), + Arrays.asList(Decls.Bytes, Decls.newListType(Decls.newListType(type))), + Decls.Bytes)); + } + decls.add(Decls.newFunction("format", formatOverloads)); + + return Collections.unmodifiableList(decls); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/celext/CustomOverload.java b/src/main/java/build/buf/protovalidate/internal/celext/CustomOverload.java new file mode 100644 index 000000000..88d08d4be --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/celext/CustomOverload.java @@ -0,0 +1,489 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.celext; + +import com.google.common.base.Ascii; +import com.google.common.base.Splitter; +import com.google.common.net.InetAddresses; +import com.google.common.primitives.Bytes; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.BytesT; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.Types; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.interpreter.functions.Overload; +import org.projectnessie.cel.interpreter.functions.UnaryOp; + +/** Defines custom function overloads (the implementation). */ +final class CustomOverload { + /** + * Create custom function overload list. + * + * @return an array of overloaded functions. + */ + static Overload[] create() { + return new Overload[] { + binaryFormat(), + unaryUnique(), + binaryStartsWith(), + binaryEndsWith(), + binaryContains(), + binaryIsHostname(), + unaryIsEmail(), + isIp(), + isUri(), + isUriRef() + }; + } + + /** + * Creates a custom binary function overload for the "format" operation. + * + * @return The {@link Overload} instance for the "format" operation. + */ + private static Overload binaryFormat() { + return Overload.binary( + "format", + (lhs, rhs) -> { + if (rhs.type() != ListT.ListType) { + return Err.newErr("format: expected list"); + } + ListT list = (ListT) rhs.convertToType(ListT.ListType); + String formatString = lhs.value().toString(); + Val status = Format.format(formatString, list); + if (status.type() == Err.ErrType) { + return status; + } + return StringT.stringOf(status.value().toString()); + }); + } + + /** + * Creates a custom unary function overload for the "unique" operation. + * + * @return The {@link Overload} instance for the "unique" operation. + */ + private static Overload unaryUnique() { + return Overload.unary( + "unique", + (val) -> { + switch (val.type().typeEnum()) { + case List: + Lister lister = (Lister) val; + if (lister.size().intValue() == 0L) { + // Uniqueness for empty lists are true. + return BoolT.True; + } + Val firstValue = lister.get(IntT.intOf(0)); + return unaryOpForPrimitiveVal(firstValue).invoke(lister); + case Bool: + case Bytes: + case Double: + case Int: + case String: + case Uint: + return unaryOpForPrimitiveVal(val).invoke(val); + default: + return Err.maybeNoSuchOverloadErr(val); + } + }); + } + + /** + * Creates a custom binary function overload for the "startsWith" operation. + * + * @return The {@link Overload} instance for the "startsWith" operation. + */ + private static Overload binaryStartsWith() { + return Overload.binary( + "startsWith", + (lhs, rhs) -> { + if (lhs.type() == StringT.StringType && rhs.type() == StringT.StringType) { + String receiver = lhs.value().toString(); + String param = rhs.value().toString(); + return receiver.startsWith(param) ? BoolT.True : BoolT.False; + } else if (lhs.type() == BytesT.BytesType && rhs.type() == BytesT.BytesType) { + byte[] receiver = (byte[]) lhs.value(); + byte[] param = (byte[]) rhs.value(); + if (receiver.length < param.length) { + return BoolT.False; + } + for (int i = 0; i < param.length; i++) { + if (param[i] != receiver[i]) { + return BoolT.False; + } + } + return BoolT.True; + } + return Err.newErr("using startsWith on a non-byte and non-string type"); + }); + } + + /** + * Creates a custom binary function overload for the "endsWith" operation. + * + * @return The {@link Overload} instance for the "endsWith" operation. + */ + private static Overload binaryEndsWith() { + return Overload.binary( + "endsWith", + (lhs, rhs) -> { + if (lhs.type() == StringT.StringType && rhs.type() == StringT.StringType) { + String receiver = lhs.value().toString(); + String param = rhs.value().toString(); + return receiver.endsWith(param) ? BoolT.True : BoolT.False; + } else if (lhs.type() == BytesT.BytesType && rhs.type() == BytesT.BytesType) { + byte[] receiver = (byte[]) lhs.value(); + byte[] param = (byte[]) rhs.value(); + if (receiver.length < param.length) { + return BoolT.False; + } + for (int i = 0; i < param.length; i++) { + if (param[param.length - i - 1] != receiver[receiver.length - i - 1]) { + return BoolT.False; + } + } + return BoolT.True; + } + return Err.newErr("using endsWith on a non-byte and non-string type"); + }); + } + + /** + * Creates a custom binary function overload for the "contains" operation. + * + * @return The {@link Overload} instance for the "contains" operation. + */ + private static Overload binaryContains() { + return Overload.binary( + "contains", + (lhs, rhs) -> { + if (lhs.type() == StringT.StringType && rhs.type() == StringT.StringType) { + String receiver = lhs.value().toString(); + String param = rhs.value().toString(); + return receiver.contains(param) ? BoolT.True : BoolT.False; + } else if (lhs.type() == BytesT.BytesType && rhs.type() == BytesT.BytesType) { + byte[] receiver = (byte[]) lhs.value(); + byte[] param = (byte[]) rhs.value(); + return Bytes.indexOf(receiver, param) == -1 ? BoolT.False : BoolT.True; + } + return Err.newErr("using contains on a non-byte and non-string type"); + }); + } + + /** + * Creates a custom binary function overload for the "isHostname" operation. + * + * @return The {@link Overload} instance for the "isHostname" operation. + */ + private static Overload binaryIsHostname() { + return Overload.unary( + "isHostname", + value -> { + String host = value.value().toString(); + if (host.isEmpty()) { + return BoolT.False; + } + return Types.boolOf(validateHostname(host)); + }); + } + + /** + * Creates a custom unary function overload for the "isEmail" operation. + * + * @return The {@link Overload} instance for the "isEmail" operation. + */ + private static Overload unaryIsEmail() { + return Overload.unary( + "isEmail", + value -> { + String addr = value.value().toString(); + if (addr.isEmpty()) { + return BoolT.False; + } + return Types.boolOf(validateEmail(addr)); + }); + } + + /** + * Creates a custom function overload for the "isIp" operation. + * + * @return The {@link Overload} instance for the "isIp" operation. + */ + private static Overload isIp() { + return Overload.overload( + "isIp", + null, + value -> { + String addr = value.value().toString(); + if (addr.isEmpty()) { + return BoolT.False; + } + return Types.boolOf(validateIP(addr, 0L)); + }, + (lhs, rhs) -> { + String address = lhs.value().toString(); + if (address.isEmpty()) { + return BoolT.False; + } + return Types.boolOf(validateIP(address, rhs.intValue())); + }, + null); + } + + /** + * Creates a custom unary function overload for the "isUri" operation. + * + * @return The {@link Overload} instance for the "isUri" operation. + */ + private static Overload isUri() { + return Overload.unary( + "isUri", + value -> { + String addr = value.value().toString(); + if (addr.isEmpty()) { + return BoolT.False; + } + try { + return Types.boolOf(new URL(addr).toURI().isAbsolute()); + } catch (MalformedURLException | URISyntaxException e) { + return BoolT.False; + } + }); + } + + /** + * Creates a custom unary function overload for the "isUriRef" operation. + * + * @return The {@link Overload} instance for the "isUriRef" operation. + */ + private static Overload isUriRef() { + return Overload.unary( + "isUriRef", + value -> { + String addr = value.value().toString(); + if (addr.isEmpty()) { + return BoolT.False; + } + try { + // TODO: The URL api requires a host or it always fails. + String host = "http://protovalidate.buf.build"; + URL url = new URL(host + addr); + return url.getPath() != null && !url.getPath().isEmpty() ? BoolT.True : BoolT.False; + } catch (MalformedURLException e) { + return BoolT.False; + } + }); + } + + /** + * Retrieves the appropriate unary operation for a primitive value based on its type. This method + * returns the unary operation that should be applied to the given primitive value. + * + * @param val The primitive value for which to retrieve the unary operation. + * @return The {@link UnaryOp} instance representing the appropriate unary operation for the + * value. + * @throws IllegalArgumentException if the value's type is not supported. + */ + private static UnaryOp unaryOpForPrimitiveVal(Val val) { + switch (val.type().typeEnum()) { + case Bool: + return uniqueMemberOverload(BoolT.BoolType, CustomOverload::uniqueScalar); + case Bytes: + return uniqueMemberOverload(BytesT.BytesType, CustomOverload::uniqueBytes); + case Double: + return uniqueMemberOverload(DoubleT.DoubleType, CustomOverload::uniqueScalar); + case Int: + return uniqueMemberOverload(IntT.IntType, CustomOverload::uniqueScalar); + case String: + return uniqueMemberOverload(StringT.StringType, CustomOverload::uniqueScalar); + case Uint: + return uniqueMemberOverload(UintT.UintType, CustomOverload::uniqueScalar); + default: + return Err::maybeNoSuchOverloadErr; + } + } + + /** + * Creates a custom unary operation overload for processing list values with a specific item type. + * The overload ensures that the list contains unique values of the specified item type. + * + * @param itemType The type of items expected in the list. + * @param overload The function to be invoked on the unique values. + * @return The {@link UnaryOp} instance for the unique member overload. + */ + private static UnaryOp uniqueMemberOverload( + org.projectnessie.cel.common.types.ref.Type itemType, overloadFunc overload) { + return value -> { + Lister list = (Lister) value; + if (list == null || list.size().intValue() == 0L) { + return Err.noMoreElements(); + } + Val firstValue = list.get(IntT.intOf(0)); + if (firstValue.type() != itemType) { + return Err.newTypeConversionError(list.type(), itemType); + } + return overload.invoke(list); + }; + } + + @FunctionalInterface + private interface overloadFunc { + Val invoke(Lister list); + } + + /** + * Determines if the input list of bytes contains unique elements. If the list contains duplicate + * byte arrays or strings, it returns false. If the list contains unique byte arrays or strings, + * it returns true. + * + * @param list The input list to check for uniqueness. + * @return {@link BoolT}.True if the list contains unique elements, {@link BoolT}.False otherwise. + */ + private static Val uniqueBytes(Lister list) { + Set exist = new HashSet<>(); + for (int i = 0; i < list.size().intValue(); i++) { + Object val = list.get(IntT.intOf(i)).value(); + if (val instanceof byte[]) { + val = new String((byte[]) val, StandardCharsets.UTF_8); + } + if (exist.contains(val)) { + return BoolT.False; + } + exist.add(val.toString()); + } + return BoolT.True; + } + + /** + * Determines if the input list contains unique scalar values. If the list contains duplicate + * scalar values, it returns {@link BoolT}.False. If the list contains unique scalar values, it + * returns {@link BoolT}.True. + * + * @param list The input list to check for uniqueness. + * @return {@link BoolT}.True if the list contains unique scalar values, {@link BoolT}.False + * otherwise. + */ + private static Val uniqueScalar(Lister list) { + Set exist = new HashSet<>(); + for (int i = 0; i < list.size().intValue(); i++) { + Val val = list.get(IntT.intOf(i)); + if (exist.contains(val)) { + return BoolT.False; + } + exist.add(val); + } + return BoolT.True; + } + + /** + * Validates if the input string is a valid email address. + * + * @param addr The input string to validate as an email address. + * @return {@code true} if the input string is a valid email address, {@code false} otherwise. + */ + private static boolean validateEmail(String addr) { + try { + InternetAddress emailAddr = new InternetAddress(addr); + emailAddr.validate(); + if (addr.contains("<")) { + return false; + } + + addr = emailAddr.getAddress(); + if (addr.length() > 254) { + return false; + } + + String[] parts = addr.split("@", 2); + return parts[0].length() < 64 && validateHostname(parts[1]); + } catch (AddressException ex) { + return false; + } + } + + /** + * Validates if the input string is a valid hostname. + * + * @param host The input string to validate as a hostname. + * @return {@code true} if the input string is a valid hostname, {@code false} otherwise. + */ + private static boolean validateHostname(String host) { + if (host.length() > 253) { + return false; + } + + String s = Ascii.toLowerCase(host.endsWith(".") ? host.substring(0, host.length() - 1) : host); + Iterable parts = Splitter.on('.').split(s); + + for (String part : parts) { + int l = part.length(); + if (l == 0 || l > 63 || part.charAt(0) == '-' || part.charAt(l - 1) == '-') { + return false; + } + + for (int i = 0; i < part.length(); i++) { + char ch = part.charAt(i); + if ((ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-') { + return false; + } + } + } + + return true; + } + + /** + * Validates if the input string is a valid IP address. + * + * @param addr The input string to validate as an IP address. + * @param ver The IP version to validate against (0 for any version, 4 for IPv4, 6 for IPv6). + * @return {@code true} if the input string is a valid IP address of the specified version, {@code + * false} otherwise. + */ + private static boolean validateIP(String addr, long ver) { + InetAddress address; + try { + address = InetAddresses.forString(addr); + } catch (Exception e) { + return false; + } + if (ver == 0L) { + return true; + } else if (ver == 4L) { + return address instanceof Inet4Address; + } else if (ver == 6L) { + return address instanceof Inet6Address; + } + return false; + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/celext/Format.java b/src/main/java/build/buf/protovalidate/internal/celext/Format.java new file mode 100644 index 000000000..19a5758d3 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/celext/Format.java @@ -0,0 +1,315 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.celext; + +import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.List; +import org.projectnessie.cel.common.types.BoolT; +import org.projectnessie.cel.common.types.BytesT; +import org.projectnessie.cel.common.types.DoubleT; +import org.projectnessie.cel.common.types.DurationT; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IntT; +import org.projectnessie.cel.common.types.ListT; +import org.projectnessie.cel.common.types.MapT; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.StringT; +import org.projectnessie.cel.common.types.TimestampT; +import org.projectnessie.cel.common.types.UintT; +import org.projectnessie.cel.common.types.pb.Db; +import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter; +import org.projectnessie.cel.common.types.ref.Type; +import org.projectnessie.cel.common.types.ref.Val; + +/** String formatter for CEL evaluation. */ +final class Format { + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static final char[] LOWER_HEX_ARRAY = "0123456789abcdef".toCharArray(); + + /** + * Format the string with a {@link ListT}. + * + * @param fmtString the string to format. + * @param list the arguments. + * @return the formatted string in {@link Val} form. + */ + static Val format(String fmtString, ListT list) { + // StringBuilder to accumulate the formatted string + StringBuilder builder = new StringBuilder(); + int index = 0; + int argIndex = 0; + while (index < fmtString.length()) { + char c = fmtString.charAt(index++); + if (c != '%') { + // Append non-format characters directly + builder.append(c); + // Add the entire character if it's not a UTF-8 character. + if ((c & 0x80) != 0) { + // Add the rest of the UTF-8 character. + while (index < fmtString.length() && (fmtString.charAt(index) & 0xc0) == 0x80) { + builder.append(fmtString.charAt(index++)); + } + } + continue; + } + if (index >= fmtString.length()) { + return Err.newErr("format: expected format specifier"); + } + if (fmtString.charAt(index) == '%') { + // Escaped '%', append '%' and move to the next character + builder.append('%'); + index++; + continue; + } + if (argIndex >= list.size().intValue()) { + return Err.newErr("format: not enough arguments"); + } + Val arg = list.get(IntT.intOf(argIndex++)); + c = fmtString.charAt(index++); + int precision = 6; + if (c == '.') { + // parse the precision + precision = 0; + while (index < fmtString.length() + && '0' <= fmtString.charAt(index) + && fmtString.charAt(index) <= '9') { + precision = precision * 10 + (fmtString.charAt(index++) - '0'); + } + if (index >= fmtString.length()) { + return Err.newErr("format: expected format specifier"); + } + c = fmtString.charAt(index++); + } + + Val status; + switch (c) { + case 'd': + status = formatDecimal(builder, arg); + break; + case 'x': + status = formatHex(builder, arg, LOWER_HEX_ARRAY); + break; + case 'X': + status = formatHex(builder, arg, HEX_ARRAY); + break; + case 's': + status = formatString(builder, arg); + break; + case 'e': + case 'f': + case 'b': + case 'o': + default: + return Err.newErr("format: unparsable format specifier %s", c); + } + if (status.type() == Err.ErrType) { + return status; + } + } + return StringT.stringOf(builder.toString()); + } + + /** + * Converts a byte array to a hexadecimal string representation. + * + * @param bytes the byte array to convert. + * @param digits the array of hexadecimal digits. + * @return the hexadecimal string representation. + */ + private static String bytesToHex(byte[] bytes, char[] digits) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = digits[v >>> 4]; + hexChars[j * 2 + 1] = digits[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Formats a string value. + * + * @param builder the StringBuilder to append the formatted string to. + * @param val the value to format. + * @return the formatted string value. + */ + private static Val formatString(StringBuilder builder, Val val) { + if (val.type() == StringT.StringType) { + builder.append(val.value()); + return NullT.NullValue; + } else if (val.type() == BytesT.BytesType) { + builder.append(val.value()); + return NullT.NullValue; + } else { + return formatStringSafe(builder, val, false); + } + } + + /** + * Formats a string value safely for other value types. + * + * @param builder the StringBuilder to append the formatted string to. + * @param val the value to format. + * @param listType indicates if the value type is a list. + * @return the formatted string value. + */ + private static Val formatStringSafe(StringBuilder builder, Val val, boolean listType) { + Type type = val.type(); + if (type == BoolT.BoolType) { + builder.append(val.booleanValue()); + } else if (type == IntT.IntType || type == UintT.UintType) { + formatInteger(builder, Long.valueOf(val.intValue()).intValue()); + } else if (type == DoubleT.DoubleType) { + DecimalFormat format = new DecimalFormat("0.#"); + builder.append(format.format(val.value())); + } else if (type == StringT.StringType) { + builder.append("\"").append(val.value().toString()).append("\""); + } else if (type == BytesT.BytesType) { + formatBytes(builder, val); + } else if (type == DurationT.DurationType) { + formatDuration(builder, val, listType); + } else if (type == TimestampT.TimestampType) { + formatTimestamp(builder, val); + } else if (type == ListT.ListType) { + formatList(builder, val); + } else if (type == MapT.MapType) { + throw new RuntimeException("unimplemented stringSafe map type"); + } else if (type == NullT.NullType) { + throw new RuntimeException("unimplemented stringSafe null type"); + } + return val; + } + + /** + * Formats a list value. + * + * @param builder the StringBuilder to append the formatted list value to. + * @param val the value to format. + */ + private static void formatList(StringBuilder builder, Val val) { + builder.append('['); + List list = val.convertToNative(List.class); + for (int i = 0; i < list.size(); i++) { + Object obj = list.get(i); + formatStringSafe(builder, DefaultTypeAdapter.nativeToValue(Db.newDb(), null, obj), true); + if (i != list.size() - 1) { + builder.append(", "); + } + } + builder.append(']'); + } + + /** + * Formats a timestamp value. + * + * @param builder the StringBuilder to append the formatted timestamp value to. + * @param val the value to format. + */ + private static void formatTimestamp(StringBuilder builder, Val val) { + builder.append("timestamp("); + Timestamp timestamp = val.convertToNative(Timestamp.class); + builder.append(timestamp.toString()); + builder.append(")"); + } + + /** + * Formats a duration value. + * + * @param builder the StringBuilder to append the formatted duration value to. + * @param val the value to format. + * @param listType indicates if the value type is a list. + */ + private static void formatDuration(StringBuilder builder, Val val, boolean listType) { + if (listType) { + builder.append("duration(\""); + } + Duration duration = val.convertToNative(Duration.class); + + double totalSeconds = duration.getSeconds() + (duration.getNanos() / 1_000_000_000.0); + + DecimalFormat format = new DecimalFormat("0.#########"); + builder.append(format.format(totalSeconds)); + builder.append("s"); + if (listType) { + builder.append("\")"); + } + } + + /** + * Formats a byte array value. + * + * @param builder the StringBuilder to append the formatted byte array value to. + * @param val the value to format. + */ + private static void formatBytes(StringBuilder builder, Val val) { + builder + .append("\"") + .append(new String((byte[]) val.value(), StandardCharsets.UTF_8)) + .append("\""); + } + + /** + * Formats an integer value. + * + * @param builder the StringBuilder to append the formatted integer value to. + * @param value the value to format. + */ + private static void formatInteger(StringBuilder builder, int value) { + if (value < 0) { + builder.append("-"); + value = -value; + } + builder.append(value); + } + + /** + * Formats a hexadecimal value. + * + * @param builder the StringBuilder to append the formatted hexadecimal value to. + * @param val the value to format. + * @param digits the array of hexadecimal digits. + * @return the formatted hexadecimal value. + */ + private static Val formatHex(StringBuilder builder, Val val, char[] digits) { + String hexString; + if (val.type() == IntT.IntType || val.type() == UintT.UintType) { + hexString = Long.toHexString(val.intValue()); + } else if (val.type() == BytesT.BytesType) { + byte[] bytes = (byte[]) val.value(); + hexString = bytesToHex(bytes, digits); + } else if (val.type() == StringT.StringType) { + hexString = val.value().toString(); + } else { + throw new RuntimeException("formatHex: expected int or string"); + } + builder.append(hexString); + return NullT.NullType; + } + + /** + * Formats a decimal value. + * + * @param builder the StringBuilder to append the formatted decimal value to. + * @param arg the value to format. + * @return the formatted decimal value. + */ + private static Val formatDecimal(StringBuilder builder, Val arg) { + builder.append(arg.value()); + return NullT.NullValue; + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/celext/ValidateLibrary.java b/src/main/java/build/buf/protovalidate/internal/celext/ValidateLibrary.java new file mode 100644 index 000000000..ffa0110ea --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/celext/ValidateLibrary.java @@ -0,0 +1,54 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.celext; + +import build.buf.protovalidate.internal.expression.NowVariable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.EvalOption; +import org.projectnessie.cel.Library; +import org.projectnessie.cel.ProgramOption; + +/** + * Custom {@link Library} for CEL. Provides all the custom extension function definitions and + * overloads. + */ +public class ValidateLibrary implements Library { + + /** + * Returns the compile options for the CEL environment. + * + * @return the compile options. + */ + @Override + public List getCompileOptions() { + return Collections.singletonList(EnvOption.declarations(CustomDeclarations.create())); + } + + /** + * Returns the program options for the CEL program. + * + * @return the program options. + */ + @Override + public List getProgramOptions() { + return Arrays.asList( + ProgramOption.evalOptions(EvalOption.OptOptimize), + ProgramOption.globals(new NowVariable()), + ProgramOption.functions(CustomOverload.create())); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/constraints/ConstraintCache.java b/src/main/java/build/buf/protovalidate/internal/constraints/ConstraintCache.java new file mode 100644 index 000000000..80dbf9c2e --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/constraints/ConstraintCache.java @@ -0,0 +1,181 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.constraints; + +import build.buf.gen.buf.validate.FieldConstraints; +import build.buf.gen.buf.validate.priv.PrivateProto; +import build.buf.protovalidate.exceptions.CompilationException; +import build.buf.protovalidate.internal.expression.AstExpression; +import build.buf.protovalidate.internal.expression.CompiledProgram; +import build.buf.protovalidate.internal.expression.Expression; +import build.buf.protovalidate.internal.expression.Variable; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.EvalOption; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.ProgramOption; +import org.projectnessie.cel.checker.Decls; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.interpreter.Activation; + +/** A build-through cache for computed standard constraints. */ +public class ConstraintCache { + /** Partial eval options for evaluating the constraint's expression. */ + private static final ProgramOption PARTIAL_EVAL_OPTIONS = + ProgramOption.evalOptions( + EvalOption.OptTrackState, + EvalOption.OptExhaustiveEval, + EvalOption.OptOptimize, + EvalOption.OptPartialEval); + + /** + * Concurrent map for caching {@link FieldDescriptor} and their associated List of {@link + * AstExpression}. + */ + private static final Map> descriptorMap = + new ConcurrentHashMap<>(); + + /** The environment to use for evaluation. */ + private final Env env; + + /** Constructs a new build-through cache for the standard constraints. */ + public ConstraintCache(Env env) { + this.env = env; + } + + /** + * Creates the standard constraints for the given field. If forItems is true, the constraints for + * repeated list items is built instead of the constraints on the list itself. + * + * @param fieldDescriptor The field descriptor to be validated. + * @param fieldConstraints The field constraint that is used for validation. + * @param forItems The field is an item list type. + * @return The list of compiled programs. + */ + public List compile( + FieldDescriptor fieldDescriptor, FieldConstraints fieldConstraints, boolean forItems) + throws CompilationException { + Message message = resolveConstraints(fieldDescriptor, fieldConstraints, forItems); + if (message == null) { + // Message null means there were no constraints resolved. + return Collections.emptyList(); + } + Env finalEnv = + env.extend( + EnvOption.types(message.getDefaultInstanceForType()), + EnvOption.declarations( + Decls.newVar( + Variable.THIS_NAME, DescriptorMappings.getCELType(fieldDescriptor, forItems)), + Decls.newVar( + Variable.RULES_NAME, + Decls.newObjectType(message.getDescriptorForType().getFullName())))); + ProgramOption rulesOption = ProgramOption.globals(Variable.newRulesVariable(message)); + List completeProgramList = new ArrayList<>(); + for (Map.Entry entry : message.getAllFields().entrySet()) { + FieldDescriptor constraintFieldDesc = entry.getKey(); + if (!descriptorMap.containsKey(constraintFieldDesc)) { + build.buf.gen.buf.validate.priv.FieldConstraints constraints = + constraintFieldDesc.getOptions().getExtension(PrivateProto.field); + List expressions = Expression.fromPrivConstraints(constraints.getCelList()); + List astExpressions = new ArrayList<>(); + for (Expression expression : expressions) { + astExpressions.add(AstExpression.newAstExpression(finalEnv, expression)); + } + descriptorMap.put(constraintFieldDesc, astExpressions); + } + List programList = descriptorMap.get(constraintFieldDesc); + completeProgramList.addAll(programList); + } + List programs = new ArrayList<>(); + for (AstExpression astExpression : completeProgramList) { + try { + Program program = finalEnv.program(astExpression.ast, rulesOption, PARTIAL_EVAL_OPTIONS); + Program.EvalResult evalResult = program.eval(Activation.emptyActivation()); + Val value = evalResult.getVal(); + if (value != null) { + Object val = value.value(); + if (val instanceof Boolean && value.booleanValue()) { + continue; + } + if (val instanceof String && val.equals("")) { + continue; + } + } + Ast residual = finalEnv.residualAst(astExpression.ast, evalResult.getEvalDetails()); + programs.add( + new CompiledProgram(finalEnv.program(residual, rulesOption), astExpression.source)); + } catch (Exception e) { + programs.add( + new CompiledProgram( + finalEnv.program(astExpression.ast, rulesOption), astExpression.source)); + } + } + return Collections.unmodifiableList(programs); + } + + /** + * Extracts the standard constraints for the specified field. An exception is thrown if the wrong + * constraints are applied to a field (typically if there is a type-mismatch). Null is returned if + * there are no standard constraints to apply to this field. + */ + @Nullable + private Message resolveConstraints( + FieldDescriptor fieldDescriptor, FieldConstraints fieldConstraints, boolean forItems) + throws CompilationException { + // Get the oneof field descriptor from the field constraints. + FieldDescriptor oneofFieldDescriptor = + fieldConstraints.getOneofFieldDescriptor(DescriptorMappings.FIELD_CONSTRAINTS_ONEOF_DESC); + if (oneofFieldDescriptor == null) { + // If the oneof field descriptor is null there are no constraints to resolve. + return null; + } + + // Get the expected constraint descriptor based on the provided field descriptor and the flag + // indicating whether it is for items. + FieldDescriptor expectedConstraintDescriptor = + DescriptorMappings.getExpectedConstraintDescriptor(fieldDescriptor, forItems); + if (expectedConstraintDescriptor != null + && !oneofFieldDescriptor.getFullName().equals(expectedConstraintDescriptor.getFullName())) { + // If the expected constraint does not match the actual oneof constraint, throw a + // CompilationError. + throw new CompilationException( + String.format( + "expected constraint %s, got %s on field %s", + expectedConstraintDescriptor.getName(), + oneofFieldDescriptor.getName(), + fieldDescriptor.getName())); + } + + // If the expected constraint descriptor is null or if the field constraints do not have the + // oneof field descriptor + // there are no constraints to resolve, so return null. + if (expectedConstraintDescriptor == null || !fieldConstraints.hasField(oneofFieldDescriptor)) { + return null; + } + + // Return the field from the field constraints identified by the oneof field descriptor, casted + // as a Message. + return (Message) fieldConstraints.getField(oneofFieldDescriptor); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/constraints/DescriptorMappings.java b/src/main/java/build/buf/protovalidate/internal/constraints/DescriptorMappings.java new file mode 100644 index 000000000..d96cbd2c0 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/constraints/DescriptorMappings.java @@ -0,0 +1,216 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.constraints; + +import build.buf.gen.buf.validate.FieldConstraints; +import com.google.api.expr.v1alpha1.Type; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.projectnessie.cel.checker.Decls; + +/** + * DescriptorMappings provides mappings between protocol buffer descriptors and CEL declarations. + */ +public class DescriptorMappings { + /** Provides a {@link Descriptor} for {@link FieldConstraints}. */ + static final Descriptor FIELD_CONSTRAINTS_DESC = FieldConstraints.getDescriptor(); + + /** Provides the {@link OneofDescriptor} for the type union in {@link FieldConstraints}. */ + static final OneofDescriptor FIELD_CONSTRAINTS_ONEOF_DESC = + FIELD_CONSTRAINTS_DESC.getOneofs().get(0); + + /** Provides the {@link FieldDescriptor} for the map standard constraints. */ + static final FieldDescriptor MAP_FIELD_CONSTRAINTS_DESC = + FIELD_CONSTRAINTS_DESC.findFieldByName("map"); + + /** Provides the {@link FieldDescriptor} for the repeated standard constraints. */ + static final FieldDescriptor REPEATED_FIELD_CONSTRAINTS_DESC = + FIELD_CONSTRAINTS_DESC.findFieldByName("repeated"); + + /** Maps protocol buffer field kinds to their expected field constraints. */ + static final Map EXPECTED_STANDARD_CONSTRAINTS = + new HashMap<>(); + + /** + * Returns the {@link build.buf.gen.buf.validate.FieldConstraints} field that is expected for the + * given wrapper well-known type's full name. If ok is false, no standard constraints exist for + * that type. + */ + static final Map EXPECTED_WKT_CONSTRAINTS = new HashMap<>(); + + static { + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.FLOAT, FIELD_CONSTRAINTS_DESC.findFieldByName("float")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.DOUBLE, FIELD_CONSTRAINTS_DESC.findFieldByName("double")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.INT32, FIELD_CONSTRAINTS_DESC.findFieldByName("int32")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.INT64, FIELD_CONSTRAINTS_DESC.findFieldByName("int64")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.UINT32, FIELD_CONSTRAINTS_DESC.findFieldByName("uint32")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.UINT64, FIELD_CONSTRAINTS_DESC.findFieldByName("uint64")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.SINT32, FIELD_CONSTRAINTS_DESC.findFieldByName("sint32")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.SINT64, FIELD_CONSTRAINTS_DESC.findFieldByName("sint64")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.FIXED32, FIELD_CONSTRAINTS_DESC.findFieldByName("fixed32")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.FIXED64, FIELD_CONSTRAINTS_DESC.findFieldByName("fixed64")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.SFIXED32, FIELD_CONSTRAINTS_DESC.findFieldByName("sfixed32")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.SFIXED64, FIELD_CONSTRAINTS_DESC.findFieldByName("sfixed64")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.BOOL, FIELD_CONSTRAINTS_DESC.findFieldByName("bool")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.STRING, FIELD_CONSTRAINTS_DESC.findFieldByName("string")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.BYTES, FIELD_CONSTRAINTS_DESC.findFieldByName("bytes")); + EXPECTED_STANDARD_CONSTRAINTS.put( + FieldDescriptor.Type.ENUM, FIELD_CONSTRAINTS_DESC.findFieldByName("enum")); + + EXPECTED_WKT_CONSTRAINTS.put( + "google.protobuf.Any", FIELD_CONSTRAINTS_DESC.findFieldByName("any")); + EXPECTED_WKT_CONSTRAINTS.put( + "google.protobuf.Duration", FIELD_CONSTRAINTS_DESC.findFieldByName("duration")); + EXPECTED_WKT_CONSTRAINTS.put( + "google.protobuf.Timestamp", FIELD_CONSTRAINTS_DESC.findFieldByName("timestamp")); + } + + /** + * Returns the {@link FieldConstraints} field that is expected for the given protocol buffer field + * kind. + */ + @Nullable + public static FieldDescriptor expectedWrapperConstraints(String fqn) { + switch (fqn) { + case "google.protobuf.BoolValue": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.BOOL); + case "google.protobuf.BytesValue": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.BYTES); + case "google.protobuf.DoubleValue": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.DOUBLE); + case "google.protobuf.FloatValue": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.FLOAT); + case "google.protobuf.Int32Value": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.INT32); + case "google.protobuf.Int64Value": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.INT64); + case "google.protobuf.StringValue": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.STRING); + case "google.protobuf.UInt32Value": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.UINT32); + case "google.protobuf.UInt64Value": + return EXPECTED_STANDARD_CONSTRAINTS.get(FieldDescriptor.Type.UINT64); + default: + return null; + } + } + + /** + * Maps a {@link FieldDescriptor.Type} to a compatible {@link com.google.api.expr.v1alpha1.Type}. + */ + public static Type protoKindToCELType(FieldDescriptor.Type kind) { + switch (kind) { + case FLOAT: + case DOUBLE: + return Decls.newPrimitiveType(Type.PrimitiveType.DOUBLE); + case INT32: + case INT64: + case SINT32: + case SINT64: + case SFIXED32: + case SFIXED64: + case ENUM: + return Decls.newPrimitiveType(Type.PrimitiveType.INT64); + case UINT32: + case UINT64: + case FIXED32: + case FIXED64: + return Decls.newPrimitiveType(Type.PrimitiveType.UINT64); + case BOOL: + return Decls.newPrimitiveType(Type.PrimitiveType.BOOL); + case STRING: + return Decls.newPrimitiveType(Type.PrimitiveType.STRING); + case BYTES: + return Decls.newPrimitiveType(Type.PrimitiveType.BYTES); + case MESSAGE: + case GROUP: + return Type.newBuilder().setMessageType(kind.getJavaType().name()).build(); + default: + return Type.newBuilder() + .setPrimitive(Type.PrimitiveType.PRIMITIVE_TYPE_UNSPECIFIED) + .build(); + } + } + + /** + * Produces the field descriptor from the {@link FieldConstraints} 'type' oneof that matches the + * provided target field descriptor. If the returned value is null, the field does not expect any + * standard constraints. + */ + @Nullable + static FieldDescriptor getExpectedConstraintDescriptor( + FieldDescriptor fieldDescriptor, boolean forItems) { + if (fieldDescriptor.isMapField()) { + return DescriptorMappings.MAP_FIELD_CONSTRAINTS_DESC; + } else if (fieldDescriptor.isRepeated() && !forItems) { + return DescriptorMappings.REPEATED_FIELD_CONSTRAINTS_DESC; + } else if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) { + return DescriptorMappings.EXPECTED_WKT_CONSTRAINTS.get( + fieldDescriptor.getMessageType().getFullName()); + } else { + return DescriptorMappings.EXPECTED_STANDARD_CONSTRAINTS.get(fieldDescriptor.getType()); + } + } + + /** + * Resolves the CEL value type for the provided {@link FieldDescriptor}. If forItems is true, the + * type for the repeated list items is returned instead of the list type itself. + */ + static Type getCELType(FieldDescriptor fieldDescriptor, boolean forItems) { + if (!forItems) { + if (fieldDescriptor.isMapField()) { + return Decls.newMapType( + getCELType(fieldDescriptor.getMessageType().findFieldByNumber(1), true), + getCELType(fieldDescriptor.getMessageType().findFieldByNumber(2), true)); + } else if (fieldDescriptor.isRepeated()) { + return Decls.newListType(getCELType(fieldDescriptor, true)); + } + } + + if (fieldDescriptor.getType() == FieldDescriptor.Type.MESSAGE) { + String fqn = fieldDescriptor.getMessageType().getFullName(); + switch (fqn) { + case "google.protobuf.Any": + return Decls.newWellKnownType(Type.WellKnownType.ANY); + case "google.protobuf.Duration": + return Decls.newWellKnownType(Type.WellKnownType.DURATION); + case "google.protobuf.Timestamp": + return Decls.newWellKnownType(Type.WellKnownType.TIMESTAMP); + default: + return Decls.newObjectType(fieldDescriptor.getFullName()); + } + } + return DescriptorMappings.protoKindToCELType(fieldDescriptor.getType()); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/AnyEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/AnyEvaluator.java new file mode 100644 index 000000000..7218c5965 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/AnyEvaluator.java @@ -0,0 +1,88 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Message; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A specialized evaluator for applying {@link build.buf.gen.buf.validate.AnyRules} to an {@link + * com.google.protobuf.Any} message. This is handled outside CEL which attempts to hydrate {@link + * com.google.protobuf.Any}'s within an expression, breaking evaluation if the type is unknown at + * runtime. + */ +class AnyEvaluator implements Evaluator { + private final Descriptors.FieldDescriptor typeURLDescriptor; + private final Set in; + private final Set notIn; + + /** Constructs a new evaluator for {@link build.buf.gen.buf.validate.AnyRules} messages. */ + AnyEvaluator(Descriptors.FieldDescriptor typeURLDescriptor, List in, List notIn) { + this.typeURLDescriptor = typeURLDescriptor; + this.in = stringsToSet(in); + this.notIn = stringsToSet(notIn); + } + + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + Message anyValue = val.messageValue(); + if (anyValue == null) { + return ValidationResult.EMPTY; + } + List violationList = new ArrayList<>(); + String typeURL = (String) anyValue.getField(typeURLDescriptor); + if (!in.isEmpty() && !in.contains(typeURL)) { + Violation violation = + Violation.newBuilder() + .setConstraintId("any.in") + .setMessage("type URL must be in the allow list") + .build(); + violationList.add(violation); + if (failFast) { + return new ValidationResult(violationList); + } + } + if (!notIn.isEmpty() && notIn.contains(typeURL)) { + Violation violation = + Violation.newBuilder() + .setConstraintId("any.not_in") + .setMessage("type URL must not be in the block list") + .build(); + violationList.add(violation); + } + return new ValidationResult(violationList); + } + + @Override + public boolean tautology() { + return in.isEmpty() && notIn.isEmpty(); + } + + /** stringsToMap converts a string list to a set for fast lookup. */ + private static Set stringsToSet(List strings) { + if (strings.isEmpty()) { + return Collections.emptySet(); + } + return new HashSet<>(strings); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/ConstraintResolver.java b/src/main/java/build/buf/protovalidate/internal/evaluator/ConstraintResolver.java new file mode 100644 index 000000000..b84fc1948 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/ConstraintResolver.java @@ -0,0 +1,75 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.FieldConstraints; +import build.buf.gen.buf.validate.MessageConstraints; +import build.buf.gen.buf.validate.OneofConstraints; +import build.buf.gen.buf.validate.ValidateProto; +import com.google.protobuf.DescriptorProtos; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.OneofDescriptor; + +/** Manages the resolution of protovalidate constraints. */ +class ConstraintResolver { + + /** + * Resolves the constraints for a message descriptor. + * + * @param desc the message descriptor. + * @return the resolved {@link MessageConstraints}. + */ + MessageConstraints resolveMessageConstraints(Descriptor desc) { + DescriptorProtos.MessageOptions options = desc.getOptions(); + if (!options.hasExtension(ValidateProto.message)) { + return MessageConstraints.getDefaultInstance(); + } + MessageConstraints constraints = options.getExtension(ValidateProto.message); + boolean disabled = constraints.getDisabled(); + if (disabled) { + return MessageConstraints.newBuilder().setDisabled(true).build(); + } + return constraints; + } + + /** + * Resolves the constraints for a oneof descriptor. + * + * @param desc the oneof descriptor. + * @return the resolved {@link OneofConstraints}. + */ + OneofConstraints resolveOneofConstraints(OneofDescriptor desc) { + DescriptorProtos.OneofOptions options = desc.getOptions(); + if (!options.hasExtension(ValidateProto.oneof)) { + return OneofConstraints.getDefaultInstance(); + } + return options.getExtension(ValidateProto.oneof); + } + + /** + * Resolves the constraints for a field descriptor. + * + * @param desc the field descriptor. + * @return the resolved {@link FieldConstraints}. + */ + FieldConstraints resolveFieldConstraints(FieldDescriptor desc) { + DescriptorProtos.FieldOptions options = desc.getOptions(); + if (!options.hasExtension(ValidateProto.field)) { + return FieldConstraints.getDefaultInstance(); + } + return options.getExtension(ValidateProto.field); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/EnumEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/EnumEvaluator.java new file mode 100644 index 000000000..878a554d8 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/EnumEvaluator.java @@ -0,0 +1,79 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.protobuf.Descriptors; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * {@link EnumEvaluator} checks an enum value being a member of the defined values exclusively. This + * check is handled outside CEL as enums are completely type erased to integers. + */ +class EnumEvaluator implements Evaluator { + /** Captures all the defined values for this enum */ + private final Set values; + + /** + * Constructs a new evaluator for enum values. + * + * @param valueDescriptors the list of {@link Descriptors.EnumValueDescriptor} for the enum. + */ + EnumEvaluator(List valueDescriptors) { + if (valueDescriptors.isEmpty()) { + this.values = Collections.emptySet(); + } else { + this.values = + valueDescriptors.stream() + .map(Descriptors.EnumValueDescriptor::getNumber) + .collect(Collectors.toSet()); + } + } + + @Override + public boolean tautology() { + return false; + } + + /** + * Evaluates an enum value. + * + * @param val the value to evaluate. + * @param failFast indicates if the evaluation should stop on the first violation. + * @return the {@link ValidationResult} of the evaluation. + * @throws ExecutionException if an error occurs during the evaluation. + */ + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + Descriptors.EnumValueDescriptor enumValue = val.value(Descriptors.EnumValueDescriptor.class); + if (enumValue == null) { + return ValidationResult.EMPTY; + } + if (!values.contains(enumValue.getNumber())) { + return new ValidationResult( + Collections.singletonList( + Violation.newBuilder() + .setConstraintId("enum.defined_only") + .setMessage("value must be one of the defined enum values") + .build())); + } + return ValidationResult.EMPTY; + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/ErrorPathUtils.java b/src/main/java/build/buf/protovalidate/internal/evaluator/ErrorPathUtils.java new file mode 100644 index 000000000..18e68f993 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/ErrorPathUtils.java @@ -0,0 +1,56 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.Violation; +import com.google.common.base.Strings; +import com.google.errorprone.annotations.FormatMethod; +import com.google.errorprone.annotations.FormatString; +import java.util.List; +import java.util.stream.Collectors; + +/** Utility class for manipulating error paths in violations. */ +final class ErrorPathUtils { + private ErrorPathUtils() {} + + /** + * Prefixes the error paths of the given violations with a format string and its arguments. + * + * @param violations The list of violations to modify. + * @param format The format string to use as the prefix. + * @param args The arguments to apply to the format string. + * @return The modified list of violations with prefixed error paths. + */ + @FormatMethod + static List prefixErrorPaths( + List violations, @FormatString String format, Object... args) { + String prefix = String.format(format, args); + return violations.stream() + .map( + violation -> { + String fieldPath = violation.getFieldPath(); + String prefixedFieldPath; + if (fieldPath.isEmpty()) { + prefixedFieldPath = prefix; + } else if (fieldPath.charAt(0) == '[') { + prefixedFieldPath = prefix + fieldPath; + } else { + prefixedFieldPath = Strings.lenientFormat("%s.%s", prefix, fieldPath); + } + return violation.toBuilder().setFieldPath(prefixedFieldPath).build(); + }) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/build/buf/protovalidate/Evaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/Evaluator.java similarity index 58% rename from src/main/java/build/buf/protovalidate/Evaluator.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/Evaluator.java index 695623d40..d4f37123b 100644 --- a/src/main/java/build/buf/protovalidate/Evaluator.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/Evaluator.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,31 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; +import build.buf.protovalidate.ValidationResult; import build.buf.protovalidate.exceptions.ExecutionException; -import java.util.List; /** * {@link Evaluator} defines a validation evaluator. evaluator implementations may elide type * checking of the passed in value, as the types have been guaranteed during the build phase. */ -interface Evaluator { - /** - * Tautology returns true if the evaluator always succeeds. - * - * @return True if the evaluator always succeeds. - */ +public interface Evaluator { + /** Returns true if the evaluator always succeeds. */ boolean tautology(); /** * Checks that the provided val is valid. Unless failFast is true, evaluation attempts to find all - * {@link RuleViolation} present in val instead of returning only the first {@link RuleViolation}. - * - * @param val The value to validate. - * @param failFast If true, validation stops after the first failure. - * @return The result of validation on the specified value. - * @throws ExecutionException If evaluation fails to complete. + * {@link build.buf.gen.buf.validate.Violations} present in val instead of returning a {@link + * ValidationResult} on the first {@link build.buf.gen.buf.validate.Violation}. An {@link + * ExecutionException} is thrown if evaluation fails to complete. */ - List evaluate(Value val, boolean failFast) throws ExecutionException; + ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException; } diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java b/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java new file mode 100644 index 000000000..ce055f774 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/EvaluatorBuilder.java @@ -0,0 +1,386 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.Constraint; +import build.buf.gen.buf.validate.FieldConstraints; +import build.buf.gen.buf.validate.MessageConstraints; +import build.buf.gen.buf.validate.OneofConstraints; +import build.buf.gen.buf.validate.ValidateProto; +import build.buf.protovalidate.exceptions.CompilationException; +import build.buf.protovalidate.internal.constraints.ConstraintCache; +import build.buf.protovalidate.internal.constraints.DescriptorMappings; +import build.buf.protovalidate.internal.expression.AstExpression; +import build.buf.protovalidate.internal.expression.CelPrograms; +import build.buf.protovalidate.internal.expression.CompiledProgram; +import build.buf.protovalidate.internal.expression.Expression; +import build.buf.protovalidate.internal.expression.Variable; +import com.google.protobuf.Descriptors; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.InvalidProtocolBufferException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.checker.Decls; + +/** A build-through cache of message evaluators keyed off the provided descriptor. */ +public class EvaluatorBuilder { + private static final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + + static { + extensionRegistry.add(ValidateProto.message); + extensionRegistry.add(ValidateProto.field); + extensionRegistry.add(ValidateProto.oneof); + } + + private static final Map evaluatorMap = new HashMap<>(); + private final ConstraintResolver resolver = new ConstraintResolver(); + + private final Env env; + private final boolean disableLazy; + private final ConstraintCache constraints; + + /** + * Constructs a new {@link EvaluatorBuilder}. + * + * @param env The CEL environment for evaluation. + * @param disableLazy Determines whether lazy loading of evaluators is disabled. + */ + public EvaluatorBuilder(Env env, boolean disableLazy) { + this.env = env; + this.disableLazy = disableLazy; + this.constraints = new ConstraintCache(env); + } + + /** + * Returns a pre-cached {@link Evaluator} for the given descriptor or, if the descriptor is + * unknown, returns an evaluator that always throws a {@link CompilationException}. + */ + public Evaluator load(Descriptor desc) throws CompilationException { + if (disableLazy) { + return loadDescriptor(desc); + } else { + return loadOrBuildDescriptor(desc); + } + } + + /** + * Either returns a memoized {@link Evaluator} for the given descriptor, or lazily constructs a + * new one. + */ + private Evaluator build(Descriptor desc) throws CompilationException { + Evaluator eval = evaluatorMap.get(desc); + if (eval != null) { + return eval; + } + MessageEvaluator msgEval = new MessageEvaluator(); + evaluatorMap.put(desc, msgEval); + buildMessage(desc, msgEval); + return msgEval; + } + + private void buildMessage(Descriptor desc, MessageEvaluator msgEval) throws CompilationException { + try { + DynamicMessage defaultInstance = + DynamicMessage.parseFrom(desc, new byte[0], extensionRegistry); + Descriptor descriptor = defaultInstance.getDescriptorForType(); + MessageConstraints msgConstraints = resolver.resolveMessageConstraints(descriptor); + if (msgConstraints.getDisabled()) { + return; + } + processMessageExpressions(descriptor, msgConstraints, msgEval, defaultInstance); + processOneofConstraints(descriptor, msgEval); + processFields(descriptor, msgEval); + } catch (InvalidProtocolBufferException e) { + throw new CompilationException("failed to parse proto definition: " + desc.getFullName()); + } + } + + private void processMessageExpressions( + Descriptor desc, + MessageConstraints msgConstraints, + MessageEvaluator msgEval, + DynamicMessage message) + throws CompilationException { + List celList = msgConstraints.getCelList(); + if (celList.isEmpty()) { + return; + } + Env finalEnv = + env.extend( + EnvOption.types(message), + EnvOption.declarations( + Decls.newVar(Variable.THIS_NAME, Decls.newObjectType(desc.getFullName())))); + List compiledPrograms = compileConstraints(celList, finalEnv); + if (compiledPrograms.isEmpty()) { + throw new CompilationException("compile returned null"); + } + msgEval.append(new CelPrograms(compiledPrograms)); + } + + private void processOneofConstraints(Descriptor desc, MessageEvaluator msgEval) { + List oneofs = desc.getOneofs(); + for (Descriptors.OneofDescriptor oneofDesc : oneofs) { + OneofConstraints oneofConstraints = resolver.resolveOneofConstraints(oneofDesc); + OneofEvaluator oneofEvaluatorEval = + new OneofEvaluator(oneofDesc, oneofConstraints.getRequired()); + msgEval.append(oneofEvaluatorEval); + } + } + + private void processFields(Descriptor desc, MessageEvaluator msgEval) + throws CompilationException { + List fields = desc.getFields(); + for (FieldDescriptor fieldDescriptor : fields) { + FieldDescriptor descriptor = desc.findFieldByName(fieldDescriptor.getName()); + FieldConstraints fieldConstraints = resolver.resolveFieldConstraints(descriptor); + FieldEvaluator fldEval = buildField(descriptor, fieldConstraints); + msgEval.append(fldEval); + } + } + + private FieldEvaluator buildField( + FieldDescriptor fieldDescriptor, FieldConstraints fieldConstraints) + throws CompilationException { + ValueEvaluator valueEvaluatorEval = new ValueEvaluator(fieldConstraints, fieldDescriptor); + FieldEvaluator fieldEvaluator = + new FieldEvaluator( + valueEvaluatorEval, + fieldDescriptor, + fieldConstraints.getRequired(), + fieldDescriptor.hasPresence()); + buildValue(fieldDescriptor, fieldConstraints, false, fieldEvaluator.valueEvaluator); + return fieldEvaluator; + } + + private void buildValue( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + boolean forItems, + ValueEvaluator valueEvaluator) + throws CompilationException { + processFieldExpressions(fieldDescriptor, fieldConstraints, valueEvaluator); + processEmbeddedMessage(fieldDescriptor, fieldConstraints, forItems, valueEvaluator); + processWrapperConstraints(fieldDescriptor, fieldConstraints, forItems, valueEvaluator); + processStandardConstraints(fieldDescriptor, fieldConstraints, forItems, valueEvaluator); + processAnyConstraints(fieldDescriptor, fieldConstraints, forItems, valueEvaluator); + processEnumConstraints(fieldDescriptor, fieldConstraints, valueEvaluator); + processMapConstraints(fieldDescriptor, fieldConstraints, valueEvaluator); + processRepeatedConstraints(fieldDescriptor, fieldConstraints, forItems, valueEvaluator); + } + + private void processFieldExpressions( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + ValueEvaluator valueEvaluatorEval) + throws CompilationException { + List constraintsCelList = fieldConstraints.getCelList(); + if (constraintsCelList.isEmpty()) { + return; + } + List opts; + if (fieldDescriptor.getType() == FieldDescriptor.Type.MESSAGE) { + try { + DynamicMessage defaultInstance = + DynamicMessage.parseFrom( + fieldDescriptor.getMessageType(), new byte[0], extensionRegistry); + opts = + Arrays.asList( + EnvOption.types(defaultInstance), + EnvOption.declarations( + Decls.newVar( + Variable.THIS_NAME, + Decls.newObjectType(fieldDescriptor.getMessageType().getFullName())))); + } catch (InvalidProtocolBufferException e) { + throw new CompilationException("field descriptor type is invalid " + e.getMessage()); + } + } else { + opts = + Collections.singletonList( + EnvOption.declarations( + Decls.newVar( + Variable.THIS_NAME, + DescriptorMappings.protoKindToCELType(fieldDescriptor.getType())))); + } + Env finalEnv = env.extend(opts.toArray(new EnvOption[0])); + List compiledPrograms = compileConstraints(constraintsCelList, finalEnv); + if (!compiledPrograms.isEmpty()) { + valueEvaluatorEval.append(new CelPrograms(compiledPrograms)); + } + } + + private void processEmbeddedMessage( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + boolean forItems, + ValueEvaluator valueEvaluatorEval) + throws CompilationException { + if (fieldDescriptor.getType() != FieldDescriptor.Type.MESSAGE + || fieldConstraints.getSkipped() + || fieldDescriptor.isMapField() + || (fieldDescriptor.isRepeated() && !forItems)) { + return; + } + Evaluator embedEval = build(fieldDescriptor.getMessageType()); + valueEvaluatorEval.append(embedEval); + } + + private void processWrapperConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + boolean forItems, + ValueEvaluator valueEvaluatorEval) + throws CompilationException { + if (fieldDescriptor.getType() != FieldDescriptor.Type.MESSAGE + || fieldConstraints.getSkipped() + || fieldDescriptor.isMapField() + || (fieldDescriptor.isRepeated() && !forItems)) { + return; + } + FieldDescriptor expectedWrapperDescriptor = + DescriptorMappings.expectedWrapperConstraints( + fieldDescriptor.getMessageType().getFullName()); + if (expectedWrapperDescriptor == null + || !fieldConstraints.hasField(expectedWrapperDescriptor)) { + return; + } + ValueEvaluator unwrapped = new ValueEvaluator(fieldConstraints, fieldDescriptor); + buildValue( + fieldDescriptor.getMessageType().findFieldByName("value"), + fieldConstraints, + true, + unwrapped); + valueEvaluatorEval.append(unwrapped); + } + + private void processStandardConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + boolean forItems, + ValueEvaluator valueEvaluatorEval) + throws CompilationException { + List compile = + constraints.compile(fieldDescriptor, fieldConstraints, forItems); + if (compile.isEmpty()) { + return; + } + valueEvaluatorEval.append(new CelPrograms(compile)); + } + + private void processAnyConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + boolean forItems, + ValueEvaluator valueEvaluatorEval) { + if ((fieldDescriptor.isRepeated() && !forItems) + || fieldDescriptor.getType() != FieldDescriptor.Type.MESSAGE + || !fieldDescriptor.getMessageType().getFullName().equals("google.protobuf.Any")) { + return; + } + FieldDescriptor typeURLDesc = fieldDescriptor.getMessageType().findFieldByName("type_url"); + AnyEvaluator anyEvaluatorEval = + new AnyEvaluator( + typeURLDesc, + fieldConstraints.getAny().getInList(), + fieldConstraints.getAny().getNotInList()); + valueEvaluatorEval.append(anyEvaluatorEval); + } + + private void processEnumConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + ValueEvaluator valueEvaluatorEval) { + if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.ENUM) { + return; + } + if (fieldConstraints.getEnum().getDefinedOnly()) { + Descriptors.EnumDescriptor enumDescriptor = fieldDescriptor.getEnumType(); + valueEvaluatorEval.append(new EnumEvaluator(enumDescriptor.getValues())); + } + } + + private void processMapConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + ValueEvaluator valueEvaluatorEval) + throws CompilationException { + if (!fieldDescriptor.isMapField()) { + return; + } + MapEvaluator mapEval = new MapEvaluator(fieldConstraints, fieldDescriptor); + buildValue( + fieldDescriptor.getMessageType().findFieldByNumber(1), + fieldConstraints.getMap().getKeys(), + true, + mapEval.getKeyEvaluator()); + buildValue( + fieldDescriptor.getMessageType().findFieldByNumber(2), + fieldConstraints.getMap().getValues(), + true, + mapEval.getValueEvaluator()); + valueEvaluatorEval.append(mapEval); + } + + private void processRepeatedConstraints( + FieldDescriptor fieldDescriptor, + FieldConstraints fieldConstraints, + boolean forItems, + ValueEvaluator valueEvaluatorEval) + throws CompilationException { + if (fieldDescriptor.isMapField() || !fieldDescriptor.isRepeated() || forItems) { + return; + } + ListEvaluator listEval = new ListEvaluator(fieldConstraints, fieldDescriptor); + buildValue( + fieldDescriptor, fieldConstraints.getRepeated().getItems(), true, listEval.itemConstraints); + valueEvaluatorEval.append(listEval); + } + + private static List compileConstraints(List constraints, Env env) + throws CompilationException { + List expressions = Expression.fromConstraints(constraints); + List compiledPrograms = new ArrayList<>(); + for (Expression expression : expressions) { + AstExpression astExpression = AstExpression.newAstExpression(env, expression); + compiledPrograms.add( + new CompiledProgram(env.program(astExpression.ast), astExpression.source)); + } + return compiledPrograms; + } + + private Evaluator loadDescriptor(Descriptor descriptor) { + Evaluator evaluator = evaluatorMap.get(descriptor); + if (evaluator == null) { + return new UnknownDescriptorEvaluator(descriptor); + } + return evaluator; + } + + private Evaluator loadOrBuildDescriptor(Descriptor descriptor) throws CompilationException { + Evaluator eval = evaluatorMap.get(descriptor); + if (eval != null) { + return eval; + } + return build(descriptor); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/FieldEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/FieldEvaluator.java new file mode 100644 index 000000000..030de07a8 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/FieldEvaluator.java @@ -0,0 +1,91 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import java.util.Collections; +import java.util.List; + +/** Performs validation on a single message field, defined by its descriptor. */ +class FieldEvaluator implements Evaluator { + /** The {@link ValueEvaluator} to apply to the field's value */ + public final ValueEvaluator valueEvaluator; + + /** The {@link FieldDescriptor} targeted by this evaluator */ + private final FieldDescriptor descriptor; + + /** Indicates that the field must have a set value. */ + private final boolean required; + + /** + * Indicates that the evaluators should not be applied to this field if the value is unset. Fields + * that contain messages, are prefixed with `optional`, or are part of a oneof are considered + * optional. evaluators will still be applied if the field is set as the zero value. + */ + private final boolean optional; + + /** Constructs a new {@link FieldEvaluator} */ + FieldEvaluator( + ValueEvaluator valueEvaluator, + FieldDescriptor descriptor, + boolean required, + boolean optional) { + this.valueEvaluator = valueEvaluator; + this.descriptor = descriptor; + this.required = required; + this.optional = optional; + } + + @Override + public boolean tautology() { + return !required && valueEvaluator.tautology(); + } + + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + Message message = val.messageValue(); + if (message == null) { + return ValidationResult.EMPTY; + } + boolean hasField; + if (descriptor.isRepeated()) { + hasField = message.getRepeatedFieldCount(descriptor) != 0; + } else { + hasField = message.hasField(descriptor); + } + if (required && !hasField) { + return new ValidationResult( + Collections.singletonList( + Violation.newBuilder() + .setFieldPath(descriptor.getName()) + .setConstraintId("required") + .setMessage("value is required") + .build())); + } + if ((optional || valueEvaluator.getIgnoreEmpty()) && !hasField) { + return ValidationResult.EMPTY; + } + Object fieldValue = message.getField(descriptor); + ValidationResult evalResult = + valueEvaluator.evaluate(new ObjectValue(descriptor, fieldValue), failFast); + List violations = + ErrorPathUtils.prefixErrorPaths(evalResult.getViolations(), "%s", descriptor.getName()); + return new ValidationResult(violations); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/ListEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/ListEvaluator.java new file mode 100644 index 000000000..fe248a084 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/ListEvaluator.java @@ -0,0 +1,64 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.FieldConstraints; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.protobuf.Descriptors; +import java.util.ArrayList; +import java.util.List; + +/** Performs validation on the elements of a repeated field. */ +class ListEvaluator implements Evaluator { + + /** Constraints are checked on every item of the list. */ + final ValueEvaluator itemConstraints; + + /** + * Constructs a {@link ListEvaluator}. + * + * @param fieldConstraints The field constraints to apply to each item in the list. + * @param fieldDescriptor The descriptor of the repeated field being evaluated. + */ + ListEvaluator(FieldConstraints fieldConstraints, Descriptors.FieldDescriptor fieldDescriptor) { + this.itemConstraints = new ValueEvaluator(fieldConstraints, fieldDescriptor); + } + + @Override + public boolean tautology() { + return itemConstraints.tautology(); + } + + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + List allViolations = new ArrayList<>(); + List repeatedValues = val.repeatedValue(); + for (int i = 0; i < repeatedValues.size(); i++) { + ValidationResult evalResult = itemConstraints.evaluate(repeatedValues.get(i), failFast); + if (evalResult.getViolations().isEmpty()) { + continue; + } + List violations = + ErrorPathUtils.prefixErrorPaths(evalResult.getViolations(), "[%d]", i); + if (failFast && !violations.isEmpty()) { + return evalResult; + } + allViolations.addAll(violations); + } + return new ValidationResult(allViolations); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/MapEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/MapEvaluator.java new file mode 100644 index 000000000..ef4f36534 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/MapEvaluator.java @@ -0,0 +1,119 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.FieldConstraints; +import build.buf.gen.buf.validate.MapRules; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.protobuf.Descriptors; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** Performs validation on a map field's key-value pairs. */ +class MapEvaluator implements Evaluator { + /** Constraint for checking the map keys */ + private final ValueEvaluator keyEvaluator; + + /** Constraint for checking the map values */ + private final ValueEvaluator valueEvaluator; + + /** + * Constructs a {@link MapEvaluator}. + * + * @param fieldConstraints The field constraints to apply to the map. + * @param fieldDescriptor The descriptor of the map field being evaluated. + */ + MapEvaluator(FieldConstraints fieldConstraints, Descriptors.FieldDescriptor fieldDescriptor) { + MapRules map = fieldConstraints.getMap(); + this.keyEvaluator = + new ValueEvaluator(map.getKeys(), fieldDescriptor.getMessageType().findFieldByNumber(1)); + this.valueEvaluator = + new ValueEvaluator(map.getValues(), fieldDescriptor.getMessageType().findFieldByNumber(2)); + } + + /** + * Gets the key evaluator associated with this map evaluator. + * + * @return The key evaluator. + */ + public ValueEvaluator getKeyEvaluator() { + return keyEvaluator; + } + + /** + * Gets the value evaluator associated with this map evaluator. + * + * @return The value evaluator. + */ + public ValueEvaluator getValueEvaluator() { + return valueEvaluator; + } + + @Override + public boolean tautology() { + return keyEvaluator.tautology() && valueEvaluator.tautology(); + } + + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + List violations = new ArrayList<>(); + Map mapValue = val.mapValue(); + for (Map.Entry entry : mapValue.entrySet()) { + violations.addAll(evalPairs(entry.getKey(), entry.getValue(), failFast)); + if (failFast && !violations.isEmpty()) { + return new ValidationResult(violations); + } + } + if (violations.isEmpty()) { + return ValidationResult.EMPTY; + } + return new ValidationResult(violations); + } + + private List evalPairs(Value key, Value value, boolean failFast) + throws ExecutionException { + List keyViolations = keyEvaluator.evaluate(key, failFast).getViolations(); + final List valueViolations; + if (failFast && !keyViolations.isEmpty()) { + // Don't evaluate value constraints if failFast is enabled and keys failed validation. + // We still need to continue execution to the end to properly prefix violation field paths. + valueViolations = Collections.emptyList(); + } else { + valueViolations = valueEvaluator.evaluate(value, failFast).getViolations(); + } + if (keyViolations.isEmpty() && valueViolations.isEmpty()) { + return Collections.emptyList(); + } + List violations = new ArrayList<>(keyViolations.size() + valueViolations.size()); + violations.addAll(keyViolations); + violations.addAll(valueViolations); + + Object keyName = key.value(Object.class); + if (keyName == null) { + return Collections.emptyList(); + } + List prefixedViolations; + if (keyName instanceof Number) { + prefixedViolations = ErrorPathUtils.prefixErrorPaths(violations, "[%s]", keyName); + } else { + prefixedViolations = ErrorPathUtils.prefixErrorPaths(violations, "[\"%s\"]", keyName); + } + return prefixedViolations; + } +} diff --git a/src/main/java/build/buf/protovalidate/MessageEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/MessageEvaluator.java similarity index 63% rename from src/main/java/build/buf/protovalidate/MessageEvaluator.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/MessageEvaluator.java index 150cae210..8624865cd 100644 --- a/src/main/java/build/buf/protovalidate/MessageEvaluator.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/MessageEvaluator.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,14 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; import build.buf.protovalidate.exceptions.ExecutionException; import java.util.ArrayList; import java.util.List; /** Performs validation on a {@link com.google.protobuf.Message}. */ -final class MessageEvaluator implements Evaluator { +class MessageEvaluator implements Evaluator { /** List of {@link Evaluator}s that are applied to a message. */ private final List evaluators = new ArrayList<>(); @@ -34,20 +36,19 @@ public boolean tautology() { } @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - List allViolations = new ArrayList<>(); + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + List violations = new ArrayList<>(); for (Evaluator evaluator : evaluators) { - List violations = evaluator.evaluate(val, failFast); - if (failFast && !violations.isEmpty()) { - return violations; + ValidationResult evalResult = evaluator.evaluate(val, failFast); + if (failFast && !evalResult.getViolations().isEmpty()) { + return evalResult; } - allViolations.addAll(violations); + violations.addAll(evalResult.getViolations()); } - if (allViolations.isEmpty()) { - return RuleViolation.NO_VIOLATIONS; + if (violations.isEmpty()) { + return ValidationResult.EMPTY; } - return allViolations; + return new ValidationResult(violations); } /** @@ -55,10 +56,7 @@ public List evaluate(Value val, boolean failFast) * * @param eval The evaluator to append. */ - void append(Evaluator eval) { - if (eval.tautology()) { - return; - } + public void append(Evaluator eval) { evaluators.add(eval); } } diff --git a/src/main/java/build/buf/protovalidate/MessageValue.java b/src/main/java/build/buf/protovalidate/internal/evaluator/MessageValue.java similarity index 72% rename from src/main/java/build/buf/protovalidate/MessageValue.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/MessageValue.java index fa96e3b8c..a347a33b0 100644 --- a/src/main/java/build/buf/protovalidate/MessageValue.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/MessageValue.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,17 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; -import com.google.protobuf.Descriptors; import com.google.protobuf.Message; import java.util.Collections; import java.util.List; import java.util.Map; -import org.jspecify.annotations.Nullable; -/** The {@link Value} type that contains a {@link com.google.protobuf.Message}. */ -final class MessageValue implements Value { +/** + * The {@link build.buf.protovalidate.internal.evaluator.Value} type that contains a {@link + * com.google.protobuf.Message}. + */ +public final class MessageValue implements Value { /** Object type since the object type is inferred from the field descriptor. */ private final Object value; @@ -32,15 +33,10 @@ final class MessageValue implements Value { * * @param value The message value. */ - MessageValue(Message value) { + public MessageValue(Message value) { this.value = value; } - @Override - public Descriptors.@Nullable FieldDescriptor fieldDescriptor() { - return null; - } - @Override public Message messageValue() { return (Message) value; @@ -51,11 +47,6 @@ public T value(Class clazz) { return clazz.cast(value); } - @Override - public Object rawValue() { - return value; - } - @Override public List repeatedValue() { return Collections.emptyList(); diff --git a/src/main/java/build/buf/protovalidate/ObjectValue.java b/src/main/java/build/buf/protovalidate/internal/evaluator/ObjectValue.java similarity index 59% rename from src/main/java/build/buf/protovalidate/ObjectValue.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/ObjectValue.java index 9a53574ac..0fbc4380f 100644 --- a/src/main/java/build/buf/protovalidate/ObjectValue.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/ObjectValue.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; import com.google.protobuf.AbstractMessage; import com.google.protobuf.Descriptors; @@ -22,10 +22,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; +import org.projectnessie.cel.common.ULong; -/** The {@link Value} type that contains a field descriptor and its value. */ -final class ObjectValue implements Value { +/** + * The {@link build.buf.protovalidate.internal.evaluator.Value} type that contains a field + * descriptor and its value. + */ +public final class ObjectValue implements Value { /** * {@link com.google.protobuf.Descriptors.FieldDescriptor} is the field descriptor for the value. @@ -36,7 +40,7 @@ final class ObjectValue implements Value { private final Object value; /** - * Constructs a new {@link ObjectValue}. + * Constructs a new {@link build.buf.protovalidate.internal.evaluator.ObjectValue}. * * @param fieldDescriptor The field descriptor for the value. * @param value The value associated with the field descriptor. @@ -46,15 +50,10 @@ final class ObjectValue implements Value { this.value = value; } - @Override - public Descriptors.FieldDescriptor fieldDescriptor() { - return fieldDescriptor; - } - @Nullable @Override public Message messageValue() { - if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) { + if (fieldDescriptor.getType() == Descriptors.FieldDescriptor.Type.MESSAGE) { return (Message) value; } return null; @@ -62,12 +61,22 @@ public Message messageValue() { @Override public T value(Class clazz) { - return clazz.cast(ProtoAdapter.toCel(fieldDescriptor, value)); - } - - @Override - public Object rawValue() { - return value; + Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType(); + if (!fieldDescriptor.isRepeated() + && (type == Descriptors.FieldDescriptor.Type.UINT32 + || type == Descriptors.FieldDescriptor.Type.UINT64 + || type == Descriptors.FieldDescriptor.Type.FIXED32 + || type == Descriptors.FieldDescriptor.Type.FIXED64)) { + /* + * Java does not have native support for unsigned int/long or uint32/uint64 types. + * To work with CEL's uint type in Java, special handling is required. + * + * When using uint32/uint64 in your protobuf objects or CEL expressions in Java, + * wrap them with the org.projectnessie.cel.common.ULong type. + */ + return clazz.cast(ULong.valueOf(((Number) value).longValue())); + } + return clazz.cast(value); } @Override @@ -76,7 +85,7 @@ public List repeatedValue() { if (fieldDescriptor.isRepeated()) { List list = (List) value; for (Object o : list) { - out.add(new ListElementValue(fieldDescriptor, o)); + out.add(new build.buf.protovalidate.internal.evaluator.ObjectValue(fieldDescriptor, o)); } } return out; @@ -94,10 +103,12 @@ public Map mapValue() { Map out = new HashMap<>(input.size()); for (AbstractMessage entry : input) { Object keyValue = entry.getField(keyDesc); - Value keyJavaValue = new ObjectValue(keyDesc, keyValue); + Value keyJavaValue = + new build.buf.protovalidate.internal.evaluator.ObjectValue(keyDesc, keyValue); Object valValue = entry.getField(valDesc); - Value valJavaValue = new ObjectValue(valDesc, valValue); + Value valJavaValue = + new build.buf.protovalidate.internal.evaluator.ObjectValue(valDesc, valValue); out.put(keyJavaValue, valJavaValue); } diff --git a/src/main/java/build/buf/protovalidate/OneofEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/OneofEvaluator.java similarity index 61% rename from src/main/java/build/buf/protovalidate/OneofEvaluator.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/OneofEvaluator.java index 5ddccd6d6..339ae9b61 100644 --- a/src/main/java/build/buf/protovalidate/OneofEvaluator.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/OneofEvaluator.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,17 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.validate.FieldPathElement; import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.protobuf.Message; import java.util.Collections; -import java.util.List; /** {@link OneofEvaluator} performs validation on a oneof union. */ -final class OneofEvaluator implements Evaluator { +public class OneofEvaluator implements Evaluator { /** The {@link OneofDescriptor} targeted by this evaluator. */ private final OneofDescriptor descriptor; @@ -35,7 +35,7 @@ final class OneofEvaluator implements Evaluator { * @param descriptor The targeted oneof descriptor. * @param required Indicates whether a member of the oneof must be set. */ - OneofEvaluator(OneofDescriptor descriptor, boolean required) { + public OneofEvaluator(OneofDescriptor descriptor, boolean required) { this.descriptor = descriptor; this.required = required; } @@ -46,17 +46,20 @@ public boolean tautology() { } @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { Message message = val.messageValue(); - if (message == null || !required || (message.getOneofFieldDescriptor(descriptor) != null)) { - return RuleViolation.NO_VIOLATIONS; + if (message == null) { + return ValidationResult.EMPTY; } - return Collections.singletonList( - RuleViolation.newBuilder() - .addFirstFieldPathElement( - FieldPathElement.newBuilder().setFieldName(descriptor.getName()).build()) - .setRuleId("required") - .setMessage("exactly one field is required in oneof")); + if (required && (message.getOneofFieldDescriptor(descriptor) == null)) { + return new ValidationResult( + Collections.singletonList( + Violation.newBuilder() + .setFieldPath(descriptor.getName()) + .setConstraintId("required") + .setMessage("exactly one field is required in oneof") + .build())); + } + return ValidationResult.EMPTY; } } diff --git a/src/main/java/build/buf/protovalidate/UnknownDescriptorEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/UnknownDescriptorEvaluator.java similarity index 68% rename from src/main/java/build/buf/protovalidate/UnknownDescriptorEvaluator.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/UnknownDescriptorEvaluator.java index 343f2e4b2..ce6a62afa 100644 --- a/src/main/java/build/buf/protovalidate/UnknownDescriptorEvaluator.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/UnknownDescriptorEvaluator.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,18 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; import build.buf.protovalidate.exceptions.ExecutionException; import com.google.protobuf.Descriptors.Descriptor; import java.util.Collections; -import java.util.List; /** * An {@link Evaluator} for an unknown descriptor. This is returned only if lazy-building of * evaluators has been disabled and an unknown descriptor is encountered. */ -final class UnknownDescriptorEvaluator implements Evaluator { +class UnknownDescriptorEvaluator implements Evaluator { /** The descriptor targeted by this evaluator. */ private final Descriptor desc; @@ -38,9 +39,11 @@ public boolean tautology() { } @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - return Collections.singletonList( - RuleViolation.newBuilder().setMessage("No evaluator available for " + desc.getFullName())); + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + return new ValidationResult( + Collections.singletonList( + Violation.newBuilder() + .setMessage("No evaluator available for " + desc.getFullName()) + .build())); } } diff --git a/src/main/java/build/buf/protovalidate/Value.java b/src/main/java/build/buf/protovalidate/internal/evaluator/Value.java similarity index 53% rename from src/main/java/build/buf/protovalidate/Value.java rename to src/main/java/build/buf/protovalidate/internal/evaluator/Value.java index a2bb96b29..0998a2ead 100644 --- a/src/main/java/build/buf/protovalidate/Value.java +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/Value.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,34 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.evaluator; -import com.google.protobuf.Descriptors; import com.google.protobuf.Message; import java.util.List; import java.util.Map; -import org.jspecify.annotations.Nullable; +import javax.annotation.Nullable; /** * {@link Value} is a wrapper around a protobuf value that provides helper methods for accessing the * value. */ -interface Value { - /** - * Get the field descriptor that corresponds to the underlying Value, if it is a message field. - * - * @return The underlying {@link Descriptors.FieldDescriptor}. null if the underlying value is not - * a message field. - */ - Descriptors.@Nullable FieldDescriptor fieldDescriptor(); - +public interface Value { /** * Get the underlying value as a {@link Message} type. * * @return The underlying {@link Message} value. null if the underlying value is not a {@link * Message} type. */ - @Nullable Message messageValue(); + @Nullable + Message messageValue(); /** * Get the underlying value and cast it to the class type. @@ -50,22 +42,6 @@ interface Value { */ T value(Class clazz); - /** - * Returns the underlying protobuf Java value without any CEL-specific adaptation. - * - *

{@link #value(Class)} routes scalars through {@code ProtoAdapter.toCel}, which converts - * {@code int32→Long}, {@code uint32→UnsignedLong}, {@code float→Double}, {@code bytes→ - * CelByteString}, etc. — appropriate for the CEL evaluation path but lossy for native rule - * evaluators that compare against raw protobuf field values. Native evaluators in {@code - * build.buf.protovalidate.rules} use this method to obtain values they can compare directly with - * the values they read off the typed rule message. - * - * @return The underlying value as protobuf-java provides it. Non-null for all values produced by - * the evaluator pipeline (field reads, list elements, message wrappers — all guarantee a - * value). - */ - Object rawValue(); - /** * Get the underlying value as a list. * diff --git a/src/main/java/build/buf/protovalidate/internal/evaluator/ValueEvaluator.java b/src/main/java/build/buf/protovalidate/internal/evaluator/ValueEvaluator.java new file mode 100644 index 000000000..ff36cfbba --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/evaluator/ValueEvaluator.java @@ -0,0 +1,120 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.evaluator; + +import build.buf.gen.buf.validate.FieldConstraints; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; +import build.buf.protovalidate.exceptions.ExecutionException; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import java.util.ArrayList; +import java.util.List; +import org.projectnessie.cel.common.ULong; + +/** + * {@link ValueEvaluator} performs validation on any concrete value contained within a singular + * field, repeated elements, or the keys/values of a map. + */ +class ValueEvaluator implements Evaluator { + private static final ULong ULONG_ZERO = ULong.valueOf(0L); + + /** The default or zero-value for this value's type. */ + private final Object zero; + + /** The evaluators applied to a value. */ + private final List evaluators = new ArrayList<>(); + + /** + * Indicates that the Constraints should not be applied if the field is unset or the default + * (typically zero) value. + */ + private final boolean ignoreEmpty; + + /** Constructs a {@link ValueEvaluator}. */ + ValueEvaluator(FieldConstraints fieldConstraints, Descriptors.FieldDescriptor fieldDescriptor) { + Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType(); + if (type == Descriptors.FieldDescriptor.Type.MESSAGE) { + DynamicMessage message = + DynamicMessage.getDefaultInstance(fieldDescriptor.getContainingType()); + this.zero = message.getField(fieldDescriptor); + } else { + if (!fieldDescriptor.isRepeated() + && (type == Descriptors.FieldDescriptor.Type.UINT32 + || type == Descriptors.FieldDescriptor.Type.UINT64 + || type == Descriptors.FieldDescriptor.Type.FIXED32 + || type == Descriptors.FieldDescriptor.Type.FIXED64)) { + this.zero = ULONG_ZERO; + } else { + this.zero = fieldDescriptor.getDefaultValue(); + } + } + this.ignoreEmpty = fieldConstraints.getIgnoreEmpty(); + } + + /** + * Gets the value of the ignoreEmpty property. + * + * @return true if empty values should be ignored, false otherwise. + */ + public boolean getIgnoreEmpty() { + return ignoreEmpty; + } + + @Override + public boolean tautology() { + return evaluators.isEmpty(); + } + + @Override + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + if (ignoreEmpty && isZero(val)) { + return ValidationResult.EMPTY; + } + List violations = new ArrayList<>(); + for (Evaluator evaluator : evaluators) { + ValidationResult evalResult = evaluator.evaluate(val, failFast); + if (failFast && !evalResult.getViolations().isEmpty()) { + return evalResult; + } + violations.addAll(evalResult.getViolations()); + } + if (violations.isEmpty()) { + return ValidationResult.EMPTY; + } + return new ValidationResult(violations); + } + + /** + * Appends an evaluator to the list of evaluators. + * + * @param eval The evaluator to append. + */ + public void append(Evaluator eval) { + if (!eval.tautology()) { + this.evaluators.add(eval); + } + } + + private boolean isZero(Value val) { + if (val == null) { + return false; + } + if (zero == null) { + return val.value(Object.class) == null; + } + return zero.equals(val.value(zero.getClass())); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/expression/AstExpression.java b/src/main/java/build/buf/protovalidate/internal/expression/AstExpression.java new file mode 100644 index 000000000..1159d3c4e --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/expression/AstExpression.java @@ -0,0 +1,60 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.expression; + +import build.buf.protovalidate.exceptions.CompilationException; +import com.google.api.expr.v1alpha1.Type; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; + +/** {@link AstExpression} is a compiled CEL {@link Ast}. */ +public class AstExpression { + /** The compiled CEL AST. */ + public final Ast ast; + + /** Contains the original expression from the proto file. */ + public final Expression source; + + /** Constructs a new {@link AstExpression}. */ + private AstExpression(Ast ast, Expression source) { + this.ast = ast; + this.source = source; + } + + /** + * Compiles the given expression to a {@link AstExpression}. + * + * @param env The CEL environment. + * @param expr The expression to compile. + * @return The compiled {@link AstExpression}. + * @throws CompilationException if the expression compilation fails. + */ + public static AstExpression newAstExpression(Env env, Expression expr) + throws CompilationException { + Env.AstIssuesTuple astIssuesTuple = env.compile(expr.expression); + if (astIssuesTuple.hasIssues()) { + throw new CompilationException("Failed to compile expression " + expr.id); + } + Ast ast = astIssuesTuple.getAst(); + Type outType = ast.getResultType(); + if (outType.getPrimitive() != Type.PrimitiveType.BOOL + && outType.getPrimitive() != Type.PrimitiveType.STRING) { + throw new CompilationException( + String.format( + "Expression outputs, wanted either bool or string: %s %s", expr.id, outType)); + } + return new AstExpression(ast, expr); + } +} diff --git a/src/main/java/build/buf/protovalidate/CelPrograms.java b/src/main/java/build/buf/protovalidate/internal/expression/CelPrograms.java similarity index 53% rename from src/main/java/build/buf/protovalidate/CelPrograms.java rename to src/main/java/build/buf/protovalidate/internal/expression/CelPrograms.java index ed712f796..f9b2faa64 100644 --- a/src/main/java/build/buf/protovalidate/CelPrograms.java +++ b/src/main/java/build/buf/protovalidate/internal/expression/CelPrograms.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,28 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build.buf.protovalidate; +package build.buf.protovalidate.internal.expression; +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.ValidationResult; import build.buf.protovalidate.exceptions.ExecutionException; -import dev.cel.runtime.CelVariableResolver; +import build.buf.protovalidate.internal.evaluator.Evaluator; +import build.buf.protovalidate.internal.evaluator.Value; import java.util.ArrayList; import java.util.List; -import org.jspecify.annotations.Nullable; /** Evaluator that executes a {@link CompiledProgram}. */ -final class CelPrograms implements Evaluator { - private final RuleViolationHelper helper; - +public class CelPrograms implements Evaluator { /** A list of {@link CompiledProgram} that will be executed against the input message. */ private final List programs; - /** - * Constructs a new {@link CelPrograms}. - * - * @param compiledPrograms The programs to execute. - */ - CelPrograms(@Nullable ValueEvaluator valueEvaluator, List compiledPrograms) { - this.helper = new RuleViolationHelper(valueEvaluator); + /** Constructs a new {@link CelPrograms}. */ + public CelPrograms(List compiledPrograms) { this.programs = compiledPrograms; } @@ -43,20 +38,18 @@ public boolean tautology() { } @Override - public List evaluate(Value val, boolean failFast) - throws ExecutionException { - CelVariableResolver bindings = Variable.newThisVariable(val.value(Object.class)); - List violations = new ArrayList<>(); + public ValidationResult evaluate(Value val, boolean failFast) throws ExecutionException { + Variable activation = Variable.newThisVariable(val.value(Object.class)); + List violationList = new ArrayList<>(); for (CompiledProgram program : programs) { - RuleViolation.Builder violation = program.eval(val, bindings); + Violation violation = program.eval(activation); if (violation != null) { - violations.add(violation); + violationList.add(violation); if (failFast) { break; } } } - return FieldPathUtils.updatePaths( - violations, helper.getFieldPathElement(), helper.getRulePrefixElements()); + return new ValidationResult(violationList); } } diff --git a/src/main/java/build/buf/protovalidate/internal/expression/CompiledProgram.java b/src/main/java/build/buf/protovalidate/internal/expression/CompiledProgram.java new file mode 100644 index 000000000..c236ddaa6 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/expression/CompiledProgram.java @@ -0,0 +1,82 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.expression; + +import build.buf.gen.buf.validate.Violation; +import build.buf.protovalidate.exceptions.ExecutionException; +import javax.annotation.Nullable; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.ref.Val; + +/** + * {@link CompiledProgram} is a parsed and type-checked {@link Program} along with the source {@link + * Expression}. + */ +public class CompiledProgram { + /** A compiled CEL program that can be evaluated against a set of variable bindings. */ + private final Program program; + + /** The original expression that was compiled into the program from the proto file. */ + private final Expression source; + + /** + * Constructs a new {@link CompiledProgram}. + * + * @param program The compiled CEL program. + * @param source The original expression that was compiled into the program. + */ + public CompiledProgram(Program program, Expression source) { + this.program = program; + this.source = source; + } + + /** + * Evaluate the compiled program with a given set of {@link Variable} bindings. + * + * @param bindings Variable bindings used for the evaluation. + * @return The {@link build.buf.gen.buf.validate.Violation} from the evaluation, or null if there + * are no violations. + * @throws ExecutionException If the evaluation of the CEL program fails with an error. + */ + @Nullable + public Violation eval(Variable bindings) throws ExecutionException { + Program.EvalResult evalResult = program.eval(bindings); + Val val = evalResult.getVal(); + if (val instanceof Err) { + throw new ExecutionException(String.format("error evaluating %s: %s", source.id, val)); + } + Object value = val.value(); + if (value instanceof String) { + if ("".equals(value)) { + return null; + } + return Violation.newBuilder() + .setConstraintId(this.source.id) + .setMessage(value.toString()) + .build(); + } else if (value instanceof Boolean) { + if (val.booleanValue()) { + return null; + } + return Violation.newBuilder() + .setConstraintId(this.source.id) + .setMessage(this.source.message) + .build(); + } else { + throw new ExecutionException(String.format("resolved to an unexpected type %s", val)); + } + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/expression/Expression.java b/src/main/java/build/buf/protovalidate/internal/expression/Expression.java new file mode 100644 index 000000000..9d51ac2c6 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/expression/Expression.java @@ -0,0 +1,92 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.expression; + +import build.buf.gen.buf.validate.priv.Constraint; +import java.util.ArrayList; +import java.util.List; + +/** Expression represents a single CEL expression. */ +public class Expression { + /** The id of the constraint. */ + public final String id; + + /** The message of the constraint. */ + public final String message; + + /** The expression of the constraint. */ + public final String expression; + + /** + * Constructs a new Expression. + * + * @param id The ID of the constraint. + * @param message The message of the constraint. + * @param expression The expression of the constraint. + */ + private Expression(String id, String message, String expression) { + this.id = id; + this.message = message; + this.expression = expression; + } + + /** + * Constructs a new Expression from the given constraint. + * + * @param constraint The constraint to create the expression from. + */ + private Expression(build.buf.gen.buf.validate.Constraint constraint) { + this(constraint.getId(), constraint.getMessage(), constraint.getExpression()); + } + + /** + * Constructs a new Expression from the given private constraint. + * + * @param constraint The private constraint to create the expression from. + */ + private Expression(build.buf.gen.buf.validate.priv.Constraint constraint) { + this(constraint.getId(), constraint.getMessage(), constraint.getExpression()); + } + + /** + * Constructs a new list of {@link Expression} from the given list of private constraints. + * + * @param constraints The list of private constraints. + * @return The list of expressions. + */ + public static List fromPrivConstraints( + List constraints) { + List expressions = new ArrayList<>(); + for (Constraint constraint : constraints) { + expressions.add(new Expression(constraint)); + } + return expressions; + } + + /** + * Constructs a new list of {@link Expression} from the given list of constraints. + * + * @param constraints The list of constraints. + * @return The list of expressions. + */ + public static List fromConstraints( + List constraints) { + List expressions = new ArrayList<>(); + for (build.buf.gen.buf.validate.Constraint constraint : constraints) { + expressions.add(new Expression(constraint)); + } + return expressions; + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/expression/NowVariable.java b/src/main/java/build/buf/protovalidate/internal/expression/NowVariable.java new file mode 100644 index 000000000..03e837125 --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/expression/NowVariable.java @@ -0,0 +1,51 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.expression; + +import java.time.Instant; +import javax.annotation.Nullable; +import org.projectnessie.cel.common.types.TimestampT; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.ResolvedValue; + +/** + * {@link NowVariable} implements {@link Activation}, providing a lazily produced timestamp for + * accessing the variable `now` that's constant within an evaluation. + */ +public class NowVariable implements Activation { + /** The name of the 'now' variable. */ + private static final String NOW_NAME = "now"; + + /** The resolved value of the 'now' variable. */ + @Nullable private ResolvedValue resolvedValue; + + @Override + public ResolvedValue resolveName(String name) { + if (!name.equals(NOW_NAME)) { + return ResolvedValue.ABSENT; + } else if (resolvedValue != null) { + return resolvedValue; + } + Instant instant = Instant.now(); // UTC. + TimestampT value = TimestampT.timestampOf(instant); + resolvedValue = ResolvedValue.resolvedValue(value); + return resolvedValue; + } + + @Override + public Activation parent() { + return Activation.emptyActivation(); + } +} diff --git a/src/main/java/build/buf/protovalidate/internal/expression/Variable.java b/src/main/java/build/buf/protovalidate/internal/expression/Variable.java new file mode 100644 index 000000000..6d458375b --- /dev/null +++ b/src/main/java/build/buf/protovalidate/internal/expression/Variable.java @@ -0,0 +1,79 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package build.buf.protovalidate.internal.expression; + +import javax.annotation.Nullable; +import org.projectnessie.cel.interpreter.Activation; +import org.projectnessie.cel.interpreter.ResolvedValue; + +/** + * {@link Variable} implements {@link org.projectnessie.cel.interpreter.Activation}, providing a + * lightweight named variable to cel.Program executions. + */ +public class Variable implements Activation { + public static final String THIS_NAME = "this"; + public static final String RULES_NAME = "rules"; + + /** The parent activation */ + private final Activation next; + + /** The variable's name */ + private final String name; + + /** The value for this variable */ + @Nullable private final Object val; + + /** Creates a new variable with the given name and value. */ + private Variable(Activation activation, String name, @Nullable Object val) { + this.next = activation; + this.name = name; + this.val = val; + } + + /** + * Creates a new "this" variable. + * + * @param val the value. + * @return {@link Variable}. + */ + public static Variable newThisVariable(@Nullable Object val) { + return new Variable(Activation.emptyActivation(), THIS_NAME, val); + } + + /** + * Creates a new "rules" variable. + * + * @param val the value. + * @return {@link Variable}. + */ + public static Variable newRulesVariable(Object val) { + return new Variable(new NowVariable(), RULES_NAME, val); + } + + @Override + public ResolvedValue resolveName(String name) { + if (this.name.equals(name)) { + return ResolvedValue.resolvedValue(val); + } else if (next != null) { + return next.resolveName(name); + } + return ResolvedValue.ABSENT; + } + + @Override + public Activation parent() { + return next; + } +} diff --git a/src/main/resources/buf/validate/validate.proto b/src/main/resources/buf/validate/validate.proto deleted file mode 100644 index 56117c88f..000000000 --- a/src/main/resources/buf/validate/validate.proto +++ /dev/null @@ -1,5130 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto2"; - -// [Protovalidate](https://protovalidate.com/) is the semantic validation library for Protobuf. -// It provides standard annotations to validate common rules on messages and fields, as well as the ability to use [CEL](https://cel.dev) to write custom rules. -// It's the next generation of [protoc-gen-validate](https://github.com/bufbuild/protoc-gen-validate). -// -// This package provides the options, messages, and enums that power Protovalidate. -// Apply its options to messages, fields, and oneofs in your Protobuf schemas to add validation rules: -// -// ```proto -// message User { -// string id = 1 [(buf.validate.field).string.uuid = true]; -// string first_name = 2 [(buf.validate.field).string.max_len = 64]; -// string last_name = 3 [(buf.validate.field).string.max_len = 64]; -// -// option (buf.validate.message).cel = { -// id: "first_name_requires_last_name" -// message: "last_name must be present if first_name is present" -// expression: "!has(this.first_name) || has(this.last_name)" -// }; -// } -// ``` -// -// These rules are enforced at runtime by language-specific libraries. -// See the [developer quickstart](https://protovalidate.com/quickstart/) to get started, or go directly to the runtime library for your language: -// [Go](https://github.com/bufbuild/protovalidate-go), -// [JavaScript/TypeScript](https://github.com/bufbuild/protovalidate-es), -// [Java](https://github.com/bufbuild/protovalidate-java), -// [Python](https://github.com/bufbuild/protovalidate-python), -// or [C++](https://github.com/bufbuild/protovalidate-cc). -package buf.validate; - -import "google/protobuf/descriptor.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/field_mask.proto"; -import "google/protobuf/timestamp.proto"; - -option go_package = "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"; -option java_multiple_files = true; -option java_outer_classname = "ValidateProto"; -option java_package = "build.buf.validate"; - -// MessageOptions is an extension to google.protobuf.MessageOptions. It allows -// the addition of validation rules at the message level. These rules can be -// applied to incoming messages to ensure they meet certain criteria before -// being processed. -extend google.protobuf.MessageOptions { - // Rules specify the validations to be performed on this message. By default, - // no validation is performed against a message. - optional MessageRules message = 1159; -} - -// OneofOptions is an extension to google.protobuf.OneofOptions. It allows -// the addition of validation rules on a oneof. These rules can be -// applied to incoming messages to ensure they meet certain criteria before -// being processed. -extend google.protobuf.OneofOptions { - // Rules specify the validations to be performed on this oneof. By default, - // no validation is performed against a oneof. - optional OneofRules oneof = 1159; -} - -// FieldOptions is an extension to google.protobuf.FieldOptions. It allows -// the addition of validation rules at the field level. These rules can be -// applied to incoming messages to ensure they meet certain criteria before -// being processed. -extend google.protobuf.FieldOptions { - // Rules specify the validations to be performed on this field. By default, - // no validation is performed against a field. - optional FieldRules field = 1159; - - // Specifies predefined rules. When extending a standard rule message, - // this adds additional CEL expressions that apply when the extension is used. - // - // ```proto - // extend buf.validate.Int32Rules { - // bool is_zero [(buf.validate.predefined).cel = { - // id: "int32.is_zero", - // message: "must be zero", - // expression: "!rule || this == 0", - // }]; - // } - // - // message Foo { - // int32 reserved = 1 [(buf.validate.field).int32.(is_zero) = true]; - // } - // ``` - optional PredefinedRules predefined = 1160; -} - -// `Rule` represents a validation rule written in the Common Expression -// Language (CEL) syntax. Each Rule includes a unique identifier, an -// optional error message, and the CEL expression to evaluate. For more -// information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/). -// -// ```proto -// message Foo { -// option (buf.validate.message).cel = { -// id: "foo.bar" -// message: "bar must be greater than 0" -// expression: "this.bar > 0" -// }; -// int32 bar = 1; -// } -// ``` -message Rule { - // `id` is a string that serves as a machine-readable name for this Rule. - // It should be unique within its scope, which could be either a message or a field. - optional string id = 1; - - // `message` is an optional field that provides a human-readable error message - // for this Rule when the CEL expression evaluates to false. If a - // non-empty message is provided, any strings resulting from the CEL - // expression evaluation are ignored. - optional string message = 2; - - // `expression` is the actual CEL expression that will be evaluated for - // validation. This string must resolve to either a boolean or a string - // value. If the expression evaluates to false or a non-empty string, the - // validation is considered failed, and the message is rejected. - optional string expression = 3; -} - -// MessageRules represents validation rules that are applied to the entire message. -// It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules. -message MessageRules { - // `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation - // rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax. - // - // This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for - // simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will - // be same as the `expression`. - // - // For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/). - // - // ```proto - // message MyMessage { - // // The field `foo` must be greater than 42. - // option (buf.validate.message).cel_expression = "this.foo > 42"; - // // The field `foo` must be less than 84. - // option (buf.validate.message).cel_expression = "this.foo < 84"; - // optional int32 foo = 1; - // } - // ``` - repeated string cel_expression = 5; - // `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message. - // These rules are written in Common Expression Language (CEL) syntax. For more information, - // [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/). - // - // - // ```proto - // message MyMessage { - // // The field `foo` must be greater than 42. - // option (buf.validate.message).cel = { - // id: "my_message.value", - // message: "must be greater than 42", - // expression: "this.foo > 42", - // }; - // optional int32 foo = 1; - // } - // ``` - repeated Rule cel = 3; - - // `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields - // of which at most one can be present. If `required` is also specified, then exactly one - // of the specified fields _must_ be present. - // - // This will enforce oneof-like constraints with a few features not provided by - // actual Protobuf oneof declarations: - // 1. Repeated and map fields are allowed in this validation. In a Protobuf oneof, - // only scalar fields are allowed. - // 2. Fields with implicit presence are allowed. In a Protobuf oneof, all member - // fields have explicit presence. This means that, for the purpose of determining - // how many fields are set, explicitly setting such a field to its zero value is - // effectively the same as not setting it at all. - // 3. This will always generate validation errors for a message unmarshalled from - // serialized data that sets more than one field. With a Protobuf oneof, when - // multiple fields are present in the serialized form, earlier values are usually - // silently ignored when unmarshalling, with only the last field being set when - // unmarshalling completes. - // - // Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means - // only the field that is set will be validated and the unset fields are not validated according to the field rules. - // This behavior can be overridden by setting `ignore` against a field. - // - // ```proto - // message MyMessage { - // // Only one of `field1` or `field2` _can_ be present in this message. - // option (buf.validate.message).oneof = { fields: ["field1", "field2"] }; - // // Exactly one of `field3` or `field4` _must_ be present in this message. - // option (buf.validate.message).oneof = { fields: ["field3", "field4"], required: true }; - // string field1 = 1; - // bytes field2 = 2; - // bool field3 = 3; - // int32 field4 = 4; - // } - // ``` - repeated MessageOneofRule oneof = 4; - - reserved 1; - reserved "disabled"; -} - -message MessageOneofRule { - // A list of field names to include in the oneof. All field names must be - // defined in the message. At least one field must be specified, and - // duplicates are not permitted. - repeated string fields = 1; - // If true, one of the fields specified _must_ be set. - optional bool required = 2; -} - -// The `OneofRules` message type enables you to manage rules for -// oneof fields in your protobuf messages. -message OneofRules { - // If `required` is true, exactly one field of the oneof must be set. A - // validation error is returned if no fields in the oneof are set. Further rules - // should be placed on the fields themselves to ensure they are valid values, - // such as `min_len` or `gt`. - // - // ```proto - // message MyMessage { - // oneof value { - // // Either `a` or `b` must be set. If `a` is set, it must also be - // // non-empty; whereas if `b` is set, it can still be an empty string. - // option (buf.validate.oneof).required = true; - // string a = 1 [(buf.validate.field).string.min_len = 1]; - // string b = 2; - // } - // } - // ``` - optional bool required = 1; -} - -// FieldRules encapsulates the rules for each type of field. Depending on -// the field, the correct set should be used to ensure proper validations. -message FieldRules { - // `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation - // rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax. - // - // This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for - // simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will - // be same as the `expression`. - // - // For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/). - // - // ```proto - // message MyMessage { - // // The field `value` must be greater than 42. - // optional int32 value = 1 [(buf.validate.field).cel_expression = "this > 42"]; - // } - // ``` - repeated string cel_expression = 29; - // `cel` is a repeated field used to represent a textual expression - // in the Common Expression Language (CEL) syntax. For more information, - // [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/). - // - // ```proto - // message MyMessage { - // // The field `value` must be greater than 42. - // optional int32 value = 1 [(buf.validate.field).cel = { - // id: "my_message.value", - // message: "must be greater than 42", - // expression: "this > 42", - // }]; - // } - // ``` - repeated Rule cel = 23; - // If `required` is true, the field must be set. A validation error is returned - // if the field is not set. - // - // ```proto - // syntax="proto3"; - // - // message FieldsWithPresence { - // // Requires any string to be set, including the empty string. - // optional string link = 1 [ - // (buf.validate.field).required = true - // ]; - // // Requires true or false to be set. - // optional bool disabled = 2 [ - // (buf.validate.field).required = true - // ]; - // // Requires a message to be set, including the empty message. - // SomeMessage msg = 4 [ - // (buf.validate.field).required = true - // ]; - // } - // ``` - // - // All fields in the example above track presence. By default, Protovalidate - // ignores rules on those fields if no value is set. `required` ensures that - // the fields are set and valid. - // - // Fields that don't track presence are always validated by Protovalidate, - // whether they are set or not. It is not necessary to add `required`. It - // can be added to indicate that the field cannot be the zero value. - // - // ```proto - // syntax="proto3"; - // - // message FieldsWithoutPresence { - // // `string.email` always applies, even to an empty string. - // string link = 1 [ - // (buf.validate.field).string.email = true - // ]; - // // `repeated.min_items` always applies, even to an empty list. - // repeated string labels = 2 [ - // (buf.validate.field).repeated.min_items = 1 - // ]; - // // `required`, for fields that don't track presence, indicates - // // the value of the field can't be the zero value. - // int32 zero_value_not_allowed = 3 [ - // (buf.validate.field).required = true - // ]; - // } - // ``` - // - // To learn which fields track presence, see the - // [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat). - // - // Note: While field rules can be applied to repeated items, map keys, and map - // values, the elements are always considered to be set. Consequently, - // specifying `repeated.items.required` is redundant. - optional bool required = 25; - // Ignore validation rules on the field if its value matches the specified - // criteria. See the `Ignore` enum for details. - // - // ```proto - // message UpdateRequest { - // // The uri rule only applies if the field is not an empty string. - // string url = 1 [ - // (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, - // (buf.validate.field).string.uri = true - // ]; - // } - // ``` - optional Ignore ignore = 27; - - oneof type { - // Scalar Field Types - FloatRules float = 1; - DoubleRules double = 2; - Int32Rules int32 = 3; - Int64Rules int64 = 4; - UInt32Rules uint32 = 5; - UInt64Rules uint64 = 6; - SInt32Rules sint32 = 7; - SInt64Rules sint64 = 8; - Fixed32Rules fixed32 = 9; - Fixed64Rules fixed64 = 10; - SFixed32Rules sfixed32 = 11; - SFixed64Rules sfixed64 = 12; - BoolRules bool = 13; - StringRules string = 14; - BytesRules bytes = 15; - - // Complex Field Types - EnumRules enum = 16; - RepeatedRules repeated = 18; - MapRules map = 19; - - // Well-Known Field Types - AnyRules any = 20; - DurationRules duration = 21; - FieldMaskRules field_mask = 28; - TimestampRules timestamp = 22; - } - - reserved 24, 26; - reserved "skipped", "ignore_empty"; -} - -// PredefinedRules are custom rules that can be re-used with -// multiple fields. -message PredefinedRules { - // `cel` is a repeated field used to represent a textual expression - // in the Common Expression Language (CEL) syntax. For more information, - // [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/). - // - // ```proto - // message MyMessage { - // // The field `value` must be greater than 42. - // optional int32 value = 1 [(buf.validate.predefined).cel = { - // id: "my_message.value", - // message: "must be greater than 42", - // expression: "this > 42", - // }]; - // } - // ``` - repeated Rule cel = 1; - - reserved 24, 26; - reserved "skipped", "ignore_empty"; -} - -// Specifies how `FieldRules.ignore` behaves, depending on the field's value, and -// whether the field tracks presence. -enum Ignore { - // Ignore rules if the field tracks presence and is unset. This is the default - // behavior. - // - // In proto3, only message fields, members of a Protobuf `oneof`, and fields - // with the `optional` label track presence. Consequently, the following fields - // are always validated, whether a value is set or not: - // - // ```proto - // syntax="proto3"; - // - // message RulesApply { - // string email = 1 [ - // (buf.validate.field).string.email = true - // ]; - // int32 age = 2 [ - // (buf.validate.field).int32.gt = 0 - // ]; - // repeated string labels = 3 [ - // (buf.validate.field).repeated.min_items = 1 - // ]; - // } - // ``` - // - // In contrast, the following fields track presence, and are only validated if - // a value is set: - // - // ```proto - // syntax="proto3"; - // - // message RulesApplyIfSet { - // optional string email = 1 [ - // (buf.validate.field).string.email = true - // ]; - // oneof ref { - // string reference = 2 [ - // (buf.validate.field).string.uuid = true - // ]; - // string name = 3 [ - // (buf.validate.field).string.min_len = 4 - // ]; - // } - // SomeMessage msg = 4 [ - // (buf.validate.field).cel = {/* ... */} - // ]; - // } - // ``` - // - // To ensure that such a field is set, add the `required` rule. - // - // To learn which fields track presence, see the - // [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat). - IGNORE_UNSPECIFIED = 0; - - // Ignore rules if the field is unset, or set to the zero value. - // - // The zero value depends on the field type: - // - For strings, the zero value is the empty string. - // - For bytes, the zero value is empty bytes. - // - For bool, the zero value is false. - // - For numeric types, the zero value is zero. - // - For enums, the zero value is the first defined enum value. - // - For repeated fields, the zero is an empty list. - // - For map fields, the zero is an empty map. - // - For message fields, absence of the message (typically a null-value) is considered zero value. - // - // For fields that track presence (e.g. adding the `optional` label in proto3), - // this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`. - IGNORE_IF_ZERO_VALUE = 1; - - // Always ignore rules, including the `required` rule. - // - // This is useful for ignoring the rules of a referenced message, or to - // temporarily ignore rules during development. - // - // ```proto - // message MyMessage { - // // The field's rules will always be ignored, including any validations - // // on value's fields. - // MyOtherMessage value = 1 [ - // (buf.validate.field).ignore = IGNORE_ALWAYS - // ]; - // } - // ``` - IGNORE_ALWAYS = 3; - - reserved 2; - reserved "IGNORE_EMPTY", "IGNORE_DEFAULT", "IGNORE_IF_DEFAULT_VALUE", "IGNORE_IF_UNPOPULATED"; -} - -// FloatRules describes the rules applied to `float` values. These -// rules may also be applied to the `google.protobuf.FloatValue` Well-Known-Type. -message FloatRules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyFloat { - // // value must equal 42.0 - // float value = 1 [(buf.validate.field).float.const = 42.0]; - // } - // ``` - optional float const = 1 [(predefined).cel = { - id: "float.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyFloat { - // // must be less than 10.0 - // float value = 1 [(buf.validate.field).float.lt = 10.0]; - // } - // ``` - float lt = 2 [(predefined).cel = { - id: "float.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this >= rules.lt)" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyFloat { - // // must be less than or equal to 10.0 - // float value = 1 [(buf.validate.field).float.lte = 10.0]; - // } - // ``` - float lte = 3 [(predefined).cel = { - id: "float.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this > rules.lte)" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyFloat { - // // must be greater than 5.0 [float.gt] - // float value = 1 [(buf.validate.field).float.gt = 5.0]; - // - // // must be greater than 5 and less than 10.0 [float.gt_lt] - // float other_value = 2 [(buf.validate.field).float = { gt: 5.0, lt: 10.0 }]; - // - // // must be greater than 10 or less than 5.0 [float.gt_lt_exclusive] - // float another_value = 3 [(buf.validate.field).float = { gt: 10.0, lt: 5.0 }]; - // } - // ``` - float gt = 4 [ - (predefined).cel = { - id: "float.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this <= rules.gt)" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "float.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this.isNan() || this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "float.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (this.isNan() || (rules.lt <= this && this <= rules.gt))" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "float.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this.isNan() || this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "float.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (this.isNan() || (rules.lte < this && this <= rules.gt))" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyFloat { - // // must be greater than or equal to 5.0 [float.gte] - // float value = 1 [(buf.validate.field).float.gte = 5.0]; - // - // // must be greater than or equal to 5.0 and less than 10.0 [float.gte_lt] - // float other_value = 2 [(buf.validate.field).float = { gte: 5.0, lt: 10.0 }]; - // - // // must be greater than or equal to 10.0 or less than 5.0 [float.gte_lt_exclusive] - // float another_value = 3 [(buf.validate.field).float = { gte: 10.0, lt: 5.0 }]; - // } - // ``` - float gte = 5 [ - (predefined).cel = { - id: "float.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this < rules.gte)" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "float.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this.isNan() || this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "float.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (this.isNan() || (rules.lt <= this && this < rules.gte))" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "float.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this.isNan() || this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "float.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (this.isNan() || (rules.lte < this && this < rules.gte))" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message - // is generated. - // - // ```proto - // message MyFloat { - // // must be in list [1.0, 2.0, 3.0] - // float value = 1 [(buf.validate.field).float = { in: [1.0, 2.0, 3.0] }]; - // } - // ``` - repeated float in = 6 [(predefined).cel = { - id: "float.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyFloat { - // // value must not be in list [1.0, 2.0, 3.0] - // float value = 1 [(buf.validate.field).float = { not_in: [1.0, 2.0, 3.0] }]; - // } - // ``` - repeated float not_in = 7 [(predefined).cel = { - id: "float.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `finite` requires the field value to be finite. If the field value is - // infinite or NaN, an error message is generated. - optional bool finite = 8 [(predefined).cel = { - id: "float.finite" - expression: "rules.finite ? (this.isNan() || this.isInf() ? 'must be finite' : '') : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyFloat { - // float value = 1 [ - // (buf.validate.field).float.example = 1.0, - // (buf.validate.field).float.example = inf - // ]; - // } - // ``` - repeated float example = 9 [(predefined).cel = { - id: "float.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// DoubleRules describes the rules applied to `double` values. These -// rules may also be applied to the `google.protobuf.DoubleValue` Well-Known-Type. -message DoubleRules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyDouble { - // // value must equal 42.0 - // double value = 1 [(buf.validate.field).double.const = 42.0]; - // } - // ``` - optional double const = 1 [(predefined).cel = { - id: "double.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyDouble { - // // must be less than 10.0 - // double value = 1 [(buf.validate.field).double.lt = 10.0]; - // } - // ``` - double lt = 2 [(predefined).cel = { - id: "double.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this >= rules.lt)" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified value - // (field <= value). If the field value is greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyDouble { - // // must be less than or equal to 10.0 - // double value = 1 [(buf.validate.field).double.lte = 10.0]; - // } - // ``` - double lte = 3 [(predefined).cel = { - id: "double.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && (this.isNan() || this > rules.lte)" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or `lte`, - // the range is reversed, and the field value must be outside the specified - // range. If the field value doesn't meet the required conditions, an error - // message is generated. - // - // ```proto - // message MyDouble { - // // must be greater than 5.0 [double.gt] - // double value = 1 [(buf.validate.field).double.gt = 5.0]; - // - // // must be greater than 5 and less than 10.0 [double.gt_lt] - // double other_value = 2 [(buf.validate.field).double = { gt: 5.0, lt: 10.0 }]; - // - // // must be greater than 10 or less than 5.0 [double.gt_lt_exclusive] - // double another_value = 3 [(buf.validate.field).double = { gt: 10.0, lt: 5.0 }]; - // } - // ``` - double gt = 4 [ - (predefined).cel = { - id: "double.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this <= rules.gt)" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "double.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this.isNan() || this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "double.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (this.isNan() || (rules.lt <= this && this <= rules.gt))" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "double.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this.isNan() || this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "double.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (this.isNan() || (rules.lte < this && this <= rules.gt))" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyDouble { - // // must be greater than or equal to 5.0 [double.gte] - // double value = 1 [(buf.validate.field).double.gte = 5.0]; - // - // // must be greater than or equal to 5.0 and less than 10.0 [double.gte_lt] - // double other_value = 2 [(buf.validate.field).double = { gte: 5.0, lt: 10.0 }]; - // - // // must be greater than or equal to 10.0 or less than 5.0 [double.gte_lt_exclusive] - // double another_value = 3 [(buf.validate.field).double = { gte: 10.0, lt: 5.0 }]; - // } - // ``` - double gte = 5 [ - (predefined).cel = { - id: "double.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && (this.isNan() || this < rules.gte)" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "double.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this.isNan() || this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "double.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (this.isNan() || (rules.lt <= this && this < rules.gte))" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "double.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this.isNan() || this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "double.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (this.isNan() || (rules.lte < this && this < rules.gte))" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MyDouble { - // // must be in list [1.0, 2.0, 3.0] - // double value = 1 [(buf.validate.field).double = { in: [1.0, 2.0, 3.0] }]; - // } - // ``` - repeated double in = 6 [(predefined).cel = { - id: "double.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyDouble { - // // value must not be in list [1.0, 2.0, 3.0] - // double value = 1 [(buf.validate.field).double = { not_in: [1.0, 2.0, 3.0] }]; - // } - // ``` - repeated double not_in = 7 [(predefined).cel = { - id: "double.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `finite` requires the field value to be finite. If the field value is - // infinite or NaN, an error message is generated. - optional bool finite = 8 [(predefined).cel = { - id: "double.finite" - expression: "rules.finite ? (this.isNan() || this.isInf() ? 'must be finite' : '') : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyDouble { - // double value = 1 [ - // (buf.validate.field).double.example = 1.0, - // (buf.validate.field).double.example = inf - // ]; - // } - // ``` - repeated double example = 9 [(predefined).cel = { - id: "double.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// Int32Rules describes the rules applied to `int32` values. These -// rules may also be applied to the `google.protobuf.Int32Value` Well-Known-Type. -message Int32Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyInt32 { - // // value must equal 42 - // int32 value = 1 [(buf.validate.field).int32.const = 42]; - // } - // ``` - optional int32 const = 1 [(predefined).cel = { - id: "int32.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field - // < value). If the field value is equal to or greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyInt32 { - // // must be less than 10 - // int32 value = 1 [(buf.validate.field).int32.lt = 10]; - // } - // ``` - int32 lt = 2 [(predefined).cel = { - id: "int32.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyInt32 { - // // must be less than or equal to 10 - // int32 value = 1 [(buf.validate.field).int32.lte = 10]; - // } - // ``` - int32 lte = 3 [(predefined).cel = { - id: "int32.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyInt32 { - // // must be greater than 5 [int32.gt] - // int32 value = 1 [(buf.validate.field).int32.gt = 5]; - // - // // must be greater than 5 and less than 10 [int32.gt_lt] - // int32 other_value = 2 [(buf.validate.field).int32 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [int32.gt_lt_exclusive] - // int32 another_value = 3 [(buf.validate.field).int32 = { gt: 10, lt: 5 }]; - // } - // ``` - int32 gt = 4 [ - (predefined).cel = { - id: "int32.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "int32.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int32.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int32.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "int32.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified value - // (exclusive). If the value of `gte` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyInt32 { - // // must be greater than or equal to 5 [int32.gte] - // int32 value = 1 [(buf.validate.field).int32.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [int32.gte_lt] - // int32 other_value = 2 [(buf.validate.field).int32 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [int32.gte_lt_exclusive] - // int32 another_value = 3 [(buf.validate.field).int32 = { gte: 10, lt: 5 }]; - // } - // ``` - int32 gte = 5 [ - (predefined).cel = { - id: "int32.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "int32.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int32.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int32.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "int32.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MyInt32 { - // // must be in list [1, 2, 3] - // int32 value = 1 [(buf.validate.field).int32 = { in: [1, 2, 3] }]; - // } - // ``` - repeated int32 in = 6 [(predefined).cel = { - id: "int32.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error message - // is generated. - // - // ```proto - // message MyInt32 { - // // value must not be in list [1, 2, 3] - // int32 value = 1 [(buf.validate.field).int32 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated int32 not_in = 7 [(predefined).cel = { - id: "int32.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyInt32 { - // int32 value = 1 [ - // (buf.validate.field).int32.example = 1, - // (buf.validate.field).int32.example = -10 - // ]; - // } - // ``` - repeated int32 example = 8 [(predefined).cel = { - id: "int32.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// Int64Rules describes the rules applied to `int64` values. These -// rules may also be applied to the `google.protobuf.Int64Value` Well-Known-Type. -message Int64Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyInt64 { - // // value must equal 42 - // int64 value = 1 [(buf.validate.field).int64.const = 42]; - // } - // ``` - optional int64 const = 1 [(predefined).cel = { - id: "int64.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyInt64 { - // // must be less than 10 - // int64 value = 1 [(buf.validate.field).int64.lt = 10]; - // } - // ``` - int64 lt = 2 [(predefined).cel = { - id: "int64.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyInt64 { - // // must be less than or equal to 10 - // int64 value = 1 [(buf.validate.field).int64.lte = 10]; - // } - // ``` - int64 lte = 3 [(predefined).cel = { - id: "int64.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyInt64 { - // // must be greater than 5 [int64.gt] - // int64 value = 1 [(buf.validate.field).int64.gt = 5]; - // - // // must be greater than 5 and less than 10 [int64.gt_lt] - // int64 other_value = 2 [(buf.validate.field).int64 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [int64.gt_lt_exclusive] - // int64 another_value = 3 [(buf.validate.field).int64 = { gt: 10, lt: 5 }]; - // } - // ``` - int64 gt = 4 [ - (predefined).cel = { - id: "int64.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "int64.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int64.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int64.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "int64.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyInt64 { - // // must be greater than or equal to 5 [int64.gte] - // int64 value = 1 [(buf.validate.field).int64.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [int64.gte_lt] - // int64 other_value = 2 [(buf.validate.field).int64 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [int64.gte_lt_exclusive] - // int64 another_value = 3 [(buf.validate.field).int64 = { gte: 10, lt: 5 }]; - // } - // ``` - int64 gte = 5 [ - (predefined).cel = { - id: "int64.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "int64.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int64.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "int64.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "int64.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MyInt64 { - // // must be in list [1, 2, 3] - // int64 value = 1 [(buf.validate.field).int64 = { in: [1, 2, 3] }]; - // } - // ``` - repeated int64 in = 6 [(predefined).cel = { - id: "int64.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyInt64 { - // // value must not be in list [1, 2, 3] - // int64 value = 1 [(buf.validate.field).int64 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated int64 not_in = 7 [(predefined).cel = { - id: "int64.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyInt64 { - // int64 value = 1 [ - // (buf.validate.field).int64.example = 1, - // (buf.validate.field).int64.example = -10 - // ]; - // } - // ``` - repeated int64 example = 9 [(predefined).cel = { - id: "int64.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// UInt32Rules describes the rules applied to `uint32` values. These -// rules may also be applied to the `google.protobuf.UInt32Value` Well-Known-Type. -message UInt32Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyUInt32 { - // // value must equal 42 - // uint32 value = 1 [(buf.validate.field).uint32.const = 42]; - // } - // ``` - optional uint32 const = 1 [(predefined).cel = { - id: "uint32.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyUInt32 { - // // must be less than 10 - // uint32 value = 1 [(buf.validate.field).uint32.lt = 10]; - // } - // ``` - uint32 lt = 2 [(predefined).cel = { - id: "uint32.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyUInt32 { - // // must be less than or equal to 10 - // uint32 value = 1 [(buf.validate.field).uint32.lte = 10]; - // } - // ``` - uint32 lte = 3 [(predefined).cel = { - id: "uint32.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyUInt32 { - // // must be greater than 5 [uint32.gt] - // uint32 value = 1 [(buf.validate.field).uint32.gt = 5]; - // - // // must be greater than 5 and less than 10 [uint32.gt_lt] - // uint32 other_value = 2 [(buf.validate.field).uint32 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [uint32.gt_lt_exclusive] - // uint32 another_value = 3 [(buf.validate.field).uint32 = { gt: 10, lt: 5 }]; - // } - // ``` - uint32 gt = 4 [ - (predefined).cel = { - id: "uint32.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "uint32.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint32.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint32.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "uint32.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyUInt32 { - // // must be greater than or equal to 5 [uint32.gte] - // uint32 value = 1 [(buf.validate.field).uint32.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [uint32.gte_lt] - // uint32 other_value = 2 [(buf.validate.field).uint32 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [uint32.gte_lt_exclusive] - // uint32 another_value = 3 [(buf.validate.field).uint32 = { gte: 10, lt: 5 }]; - // } - // ``` - uint32 gte = 5 [ - (predefined).cel = { - id: "uint32.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "uint32.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint32.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint32.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "uint32.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MyUInt32 { - // // must be in list [1, 2, 3] - // uint32 value = 1 [(buf.validate.field).uint32 = { in: [1, 2, 3] }]; - // } - // ``` - repeated uint32 in = 6 [(predefined).cel = { - id: "uint32.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyUInt32 { - // // value must not be in list [1, 2, 3] - // uint32 value = 1 [(buf.validate.field).uint32 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated uint32 not_in = 7 [(predefined).cel = { - id: "uint32.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyUInt32 { - // uint32 value = 1 [ - // (buf.validate.field).uint32.example = 1, - // (buf.validate.field).uint32.example = 10 - // ]; - // } - // ``` - repeated uint32 example = 8 [(predefined).cel = { - id: "uint32.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// UInt64Rules describes the rules applied to `uint64` values. These -// rules may also be applied to the `google.protobuf.UInt64Value` Well-Known-Type. -message UInt64Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyUInt64 { - // // value must equal 42 - // uint64 value = 1 [(buf.validate.field).uint64.const = 42]; - // } - // ``` - optional uint64 const = 1 [(predefined).cel = { - id: "uint64.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyUInt64 { - // // must be less than 10 - // uint64 value = 1 [(buf.validate.field).uint64.lt = 10]; - // } - // ``` - uint64 lt = 2 [(predefined).cel = { - id: "uint64.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyUInt64 { - // // must be less than or equal to 10 - // uint64 value = 1 [(buf.validate.field).uint64.lte = 10]; - // } - // ``` - uint64 lte = 3 [(predefined).cel = { - id: "uint64.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyUInt64 { - // // must be greater than 5 [uint64.gt] - // uint64 value = 1 [(buf.validate.field).uint64.gt = 5]; - // - // // must be greater than 5 and less than 10 [uint64.gt_lt] - // uint64 other_value = 2 [(buf.validate.field).uint64 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [uint64.gt_lt_exclusive] - // uint64 another_value = 3 [(buf.validate.field).uint64 = { gt: 10, lt: 5 }]; - // } - // ``` - uint64 gt = 4 [ - (predefined).cel = { - id: "uint64.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "uint64.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint64.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint64.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "uint64.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyUInt64 { - // // must be greater than or equal to 5 [uint64.gte] - // uint64 value = 1 [(buf.validate.field).uint64.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [uint64.gte_lt] - // uint64 other_value = 2 [(buf.validate.field).uint64 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [uint64.gte_lt_exclusive] - // uint64 another_value = 3 [(buf.validate.field).uint64 = { gte: 10, lt: 5 }]; - // } - // ``` - uint64 gte = 5 [ - (predefined).cel = { - id: "uint64.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "uint64.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint64.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "uint64.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "uint64.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MyUInt64 { - // // must be in list [1, 2, 3] - // uint64 value = 1 [(buf.validate.field).uint64 = { in: [1, 2, 3] }]; - // } - // ``` - repeated uint64 in = 6 [(predefined).cel = { - id: "uint64.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyUInt64 { - // // value must not be in list [1, 2, 3] - // uint64 value = 1 [(buf.validate.field).uint64 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated uint64 not_in = 7 [(predefined).cel = { - id: "uint64.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyUInt64 { - // uint64 value = 1 [ - // (buf.validate.field).uint64.example = 1, - // (buf.validate.field).uint64.example = -10 - // ]; - // } - // ``` - repeated uint64 example = 8 [(predefined).cel = { - id: "uint64.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// SInt32Rules describes the rules applied to `sint32` values. -message SInt32Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MySInt32 { - // // value must equal 42 - // sint32 value = 1 [(buf.validate.field).sint32.const = 42]; - // } - // ``` - optional sint32 const = 1 [(predefined).cel = { - id: "sint32.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field - // < value). If the field value is equal to or greater than the specified - // value, an error message is generated. - // - // ```proto - // message MySInt32 { - // // must be less than 10 - // sint32 value = 1 [(buf.validate.field).sint32.lt = 10]; - // } - // ``` - sint32 lt = 2 [(predefined).cel = { - id: "sint32.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MySInt32 { - // // must be less than or equal to 10 - // sint32 value = 1 [(buf.validate.field).sint32.lte = 10]; - // } - // ``` - sint32 lte = 3 [(predefined).cel = { - id: "sint32.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySInt32 { - // // must be greater than 5 [sint32.gt] - // sint32 value = 1 [(buf.validate.field).sint32.gt = 5]; - // - // // must be greater than 5 and less than 10 [sint32.gt_lt] - // sint32 other_value = 2 [(buf.validate.field).sint32 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [sint32.gt_lt_exclusive] - // sint32 another_value = 3 [(buf.validate.field).sint32 = { gt: 10, lt: 5 }]; - // } - // ``` - sint32 gt = 4 [ - (predefined).cel = { - id: "sint32.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "sint32.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint32.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint32.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sint32.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySInt32 { - // // must be greater than or equal to 5 [sint32.gte] - // sint32 value = 1 [(buf.validate.field).sint32.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [sint32.gte_lt] - // sint32 other_value = 2 [(buf.validate.field).sint32 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [sint32.gte_lt_exclusive] - // sint32 another_value = 3 [(buf.validate.field).sint32 = { gte: 10, lt: 5 }]; - // } - // ``` - sint32 gte = 5 [ - (predefined).cel = { - id: "sint32.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "sint32.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint32.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint32.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sint32.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MySInt32 { - // // must be in list [1, 2, 3] - // sint32 value = 1 [(buf.validate.field).sint32 = { in: [1, 2, 3] }]; - // } - // ``` - repeated sint32 in = 6 [(predefined).cel = { - id: "sint32.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MySInt32 { - // // value must not be in list [1, 2, 3] - // sint32 value = 1 [(buf.validate.field).sint32 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated sint32 not_in = 7 [(predefined).cel = { - id: "sint32.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MySInt32 { - // sint32 value = 1 [ - // (buf.validate.field).sint32.example = 1, - // (buf.validate.field).sint32.example = -10 - // ]; - // } - // ``` - repeated sint32 example = 8 [(predefined).cel = { - id: "sint32.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// SInt64Rules describes the rules applied to `sint64` values. -message SInt64Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MySInt64 { - // // value must equal 42 - // sint64 value = 1 [(buf.validate.field).sint64.const = 42]; - // } - // ``` - optional sint64 const = 1 [(predefined).cel = { - id: "sint64.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field - // < value). If the field value is equal to or greater than the specified - // value, an error message is generated. - // - // ```proto - // message MySInt64 { - // // must be less than 10 - // sint64 value = 1 [(buf.validate.field).sint64.lt = 10]; - // } - // ``` - sint64 lt = 2 [(predefined).cel = { - id: "sint64.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MySInt64 { - // // must be less than or equal to 10 - // sint64 value = 1 [(buf.validate.field).sint64.lte = 10]; - // } - // ``` - sint64 lte = 3 [(predefined).cel = { - id: "sint64.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySInt64 { - // // must be greater than 5 [sint64.gt] - // sint64 value = 1 [(buf.validate.field).sint64.gt = 5]; - // - // // must be greater than 5 and less than 10 [sint64.gt_lt] - // sint64 other_value = 2 [(buf.validate.field).sint64 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [sint64.gt_lt_exclusive] - // sint64 another_value = 3 [(buf.validate.field).sint64 = { gt: 10, lt: 5 }]; - // } - // ``` - sint64 gt = 4 [ - (predefined).cel = { - id: "sint64.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "sint64.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint64.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint64.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sint64.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySInt64 { - // // must be greater than or equal to 5 [sint64.gte] - // sint64 value = 1 [(buf.validate.field).sint64.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [sint64.gte_lt] - // sint64 other_value = 2 [(buf.validate.field).sint64 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [sint64.gte_lt_exclusive] - // sint64 another_value = 3 [(buf.validate.field).sint64 = { gte: 10, lt: 5 }]; - // } - // ``` - sint64 gte = 5 [ - (predefined).cel = { - id: "sint64.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "sint64.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint64.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sint64.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sint64.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message - // is generated. - // - // ```proto - // message MySInt64 { - // // must be in list [1, 2, 3] - // sint64 value = 1 [(buf.validate.field).sint64 = { in: [1, 2, 3] }]; - // } - // ``` - repeated sint64 in = 6 [(predefined).cel = { - id: "sint64.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MySInt64 { - // // value must not be in list [1, 2, 3] - // sint64 value = 1 [(buf.validate.field).sint64 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated sint64 not_in = 7 [(predefined).cel = { - id: "sint64.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MySInt64 { - // sint64 value = 1 [ - // (buf.validate.field).sint64.example = 1, - // (buf.validate.field).sint64.example = -10 - // ]; - // } - // ``` - repeated sint64 example = 8 [(predefined).cel = { - id: "sint64.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// Fixed32Rules describes the rules applied to `fixed32` values. -message Fixed32Rules { - // `const` requires the field value to exactly match the specified value. - // If the field value doesn't match, an error message is generated. - // - // ```proto - // message MyFixed32 { - // // value must equal 42 - // fixed32 value = 1 [(buf.validate.field).fixed32.const = 42]; - // } - // ``` - optional fixed32 const = 1 [(predefined).cel = { - id: "fixed32.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyFixed32 { - // // must be less than 10 - // fixed32 value = 1 [(buf.validate.field).fixed32.lt = 10]; - // } - // ``` - fixed32 lt = 2 [(predefined).cel = { - id: "fixed32.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyFixed32 { - // // must be less than or equal to 10 - // fixed32 value = 1 [(buf.validate.field).fixed32.lte = 10]; - // } - // ``` - fixed32 lte = 3 [(predefined).cel = { - id: "fixed32.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyFixed32 { - // // must be greater than 5 [fixed32.gt] - // fixed32 value = 1 [(buf.validate.field).fixed32.gt = 5]; - // - // // must be greater than 5 and less than 10 [fixed32.gt_lt] - // fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [fixed32.gt_lt_exclusive] - // fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gt: 10, lt: 5 }]; - // } - // ``` - fixed32 gt = 4 [ - (predefined).cel = { - id: "fixed32.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "fixed32.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed32.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed32.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "fixed32.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyFixed32 { - // // must be greater than or equal to 5 [fixed32.gte] - // fixed32 value = 1 [(buf.validate.field).fixed32.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [fixed32.gte_lt] - // fixed32 other_value = 2 [(buf.validate.field).fixed32 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [fixed32.gte_lt_exclusive] - // fixed32 another_value = 3 [(buf.validate.field).fixed32 = { gte: 10, lt: 5 }]; - // } - // ``` - fixed32 gte = 5 [ - (predefined).cel = { - id: "fixed32.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "fixed32.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed32.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed32.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "fixed32.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message - // is generated. - // - // ```proto - // message MyFixed32 { - // // must be in list [1, 2, 3] - // fixed32 value = 1 [(buf.validate.field).fixed32 = { in: [1, 2, 3] }]; - // } - // ``` - repeated fixed32 in = 6 [(predefined).cel = { - id: "fixed32.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyFixed32 { - // // value must not be in list [1, 2, 3] - // fixed32 value = 1 [(buf.validate.field).fixed32 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated fixed32 not_in = 7 [(predefined).cel = { - id: "fixed32.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyFixed32 { - // fixed32 value = 1 [ - // (buf.validate.field).fixed32.example = 1, - // (buf.validate.field).fixed32.example = 2 - // ]; - // } - // ``` - repeated fixed32 example = 8 [(predefined).cel = { - id: "fixed32.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// Fixed64Rules describes the rules applied to `fixed64` values. -message Fixed64Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyFixed64 { - // // value must equal 42 - // fixed64 value = 1 [(buf.validate.field).fixed64.const = 42]; - // } - // ``` - optional fixed64 const = 1 [(predefined).cel = { - id: "fixed64.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MyFixed64 { - // // must be less than 10 - // fixed64 value = 1 [(buf.validate.field).fixed64.lt = 10]; - // } - // ``` - fixed64 lt = 2 [(predefined).cel = { - id: "fixed64.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MyFixed64 { - // // must be less than or equal to 10 - // fixed64 value = 1 [(buf.validate.field).fixed64.lte = 10]; - // } - // ``` - fixed64 lte = 3 [(predefined).cel = { - id: "fixed64.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyFixed64 { - // // must be greater than 5 [fixed64.gt] - // fixed64 value = 1 [(buf.validate.field).fixed64.gt = 5]; - // - // // must be greater than 5 and less than 10 [fixed64.gt_lt] - // fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [fixed64.gt_lt_exclusive] - // fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gt: 10, lt: 5 }]; - // } - // ``` - fixed64 gt = 4 [ - (predefined).cel = { - id: "fixed64.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "fixed64.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed64.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed64.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "fixed64.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyFixed64 { - // // must be greater than or equal to 5 [fixed64.gte] - // fixed64 value = 1 [(buf.validate.field).fixed64.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [fixed64.gte_lt] - // fixed64 other_value = 2 [(buf.validate.field).fixed64 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [fixed64.gte_lt_exclusive] - // fixed64 another_value = 3 [(buf.validate.field).fixed64 = { gte: 10, lt: 5 }]; - // } - // ``` - fixed64 gte = 5 [ - (predefined).cel = { - id: "fixed64.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "fixed64.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed64.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "fixed64.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "fixed64.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MyFixed64 { - // // must be in list [1, 2, 3] - // fixed64 value = 1 [(buf.validate.field).fixed64 = { in: [1, 2, 3] }]; - // } - // ``` - repeated fixed64 in = 6 [(predefined).cel = { - id: "fixed64.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MyFixed64 { - // // value must not be in list [1, 2, 3] - // fixed64 value = 1 [(buf.validate.field).fixed64 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated fixed64 not_in = 7 [(predefined).cel = { - id: "fixed64.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyFixed64 { - // fixed64 value = 1 [ - // (buf.validate.field).fixed64.example = 1, - // (buf.validate.field).fixed64.example = 2 - // ]; - // } - // ``` - repeated fixed64 example = 8 [(predefined).cel = { - id: "fixed64.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// SFixed32Rules describes the rules applied to `fixed32` values. -message SFixed32Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MySFixed32 { - // // value must equal 42 - // sfixed32 value = 1 [(buf.validate.field).sfixed32.const = 42]; - // } - // ``` - optional sfixed32 const = 1 [(predefined).cel = { - id: "sfixed32.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MySFixed32 { - // // must be less than 10 - // sfixed32 value = 1 [(buf.validate.field).sfixed32.lt = 10]; - // } - // ``` - sfixed32 lt = 2 [(predefined).cel = { - id: "sfixed32.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MySFixed32 { - // // must be less than or equal to 10 - // sfixed32 value = 1 [(buf.validate.field).sfixed32.lte = 10]; - // } - // ``` - sfixed32 lte = 3 [(predefined).cel = { - id: "sfixed32.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySFixed32 { - // // must be greater than 5 [sfixed32.gt] - // sfixed32 value = 1 [(buf.validate.field).sfixed32.gt = 5]; - // - // // must be greater than 5 and less than 10 [sfixed32.gt_lt] - // sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [sfixed32.gt_lt_exclusive] - // sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gt: 10, lt: 5 }]; - // } - // ``` - sfixed32 gt = 4 [ - (predefined).cel = { - id: "sfixed32.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySFixed32 { - // // must be greater than or equal to 5 [sfixed32.gte] - // sfixed32 value = 1 [(buf.validate.field).sfixed32.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [sfixed32.gte_lt] - // sfixed32 other_value = 2 [(buf.validate.field).sfixed32 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [sfixed32.gte_lt_exclusive] - // sfixed32 another_value = 3 [(buf.validate.field).sfixed32 = { gte: 10, lt: 5 }]; - // } - // ``` - sfixed32 gte = 5 [ - (predefined).cel = { - id: "sfixed32.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sfixed32.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MySFixed32 { - // // must be in list [1, 2, 3] - // sfixed32 value = 1 [(buf.validate.field).sfixed32 = { in: [1, 2, 3] }]; - // } - // ``` - repeated sfixed32 in = 6 [(predefined).cel = { - id: "sfixed32.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MySFixed32 { - // // value must not be in list [1, 2, 3] - // sfixed32 value = 1 [(buf.validate.field).sfixed32 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated sfixed32 not_in = 7 [(predefined).cel = { - id: "sfixed32.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MySFixed32 { - // sfixed32 value = 1 [ - // (buf.validate.field).sfixed32.example = 1, - // (buf.validate.field).sfixed32.example = 2 - // ]; - // } - // ``` - repeated sfixed32 example = 8 [(predefined).cel = { - id: "sfixed32.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// SFixed64Rules describes the rules applied to `fixed64` values. -message SFixed64Rules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MySFixed64 { - // // value must equal 42 - // sfixed64 value = 1 [(buf.validate.field).sfixed64.const = 42]; - // } - // ``` - optional sfixed64 const = 1 [(predefined).cel = { - id: "sfixed64.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the field value to be less than the specified value (field < - // value). If the field value is equal to or greater than the specified value, - // an error message is generated. - // - // ```proto - // message MySFixed64 { - // // must be less than 10 - // sfixed64 value = 1 [(buf.validate.field).sfixed64.lt = 10]; - // } - // ``` - sfixed64 lt = 2 [(predefined).cel = { - id: "sfixed64.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the field value to be less than or equal to the specified - // value (field <= value). If the field value is greater than the specified - // value, an error message is generated. - // - // ```proto - // message MySFixed64 { - // // must be less than or equal to 10 - // sfixed64 value = 1 [(buf.validate.field).sfixed64.lte = 10]; - // } - // ``` - sfixed64 lte = 3 [(predefined).cel = { - id: "sfixed64.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the field value to be greater than the specified value - // (exclusive). If the value of `gt` is larger than a specified `lt` or - // `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySFixed64 { - // // must be greater than 5 [sfixed64.gt] - // sfixed64 value = 1 [(buf.validate.field).sfixed64.gt = 5]; - // - // // must be greater than 5 and less than 10 [sfixed64.gt_lt] - // sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gt: 5, lt: 10 }]; - // - // // must be greater than 10 or less than 5 [sfixed64.gt_lt_exclusive] - // sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gt: 10, lt: 5 }]; - // } - // ``` - sfixed64 gt = 4 [ - (predefined).cel = { - id: "sfixed64.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the field value to be greater than or equal to the specified - // value (exclusive). If the value of `gte` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MySFixed64 { - // // must be greater than or equal to 5 [sfixed64.gte] - // sfixed64 value = 1 [(buf.validate.field).sfixed64.gte = 5]; - // - // // must be greater than or equal to 5 and less than 10 [sfixed64.gte_lt] - // sfixed64 other_value = 2 [(buf.validate.field).sfixed64 = { gte: 5, lt: 10 }]; - // - // // must be greater than or equal to 10 or less than 5 [sfixed64.gte_lt_exclusive] - // sfixed64 another_value = 3 [(buf.validate.field).sfixed64 = { gte: 10, lt: 5 }]; - // } - // ``` - sfixed64 gte = 5 [ - (predefined).cel = { - id: "sfixed64.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "sfixed64.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` requires the field value to be equal to one of the specified values. - // If the field value isn't one of the specified values, an error message is - // generated. - // - // ```proto - // message MySFixed64 { - // // must be in list [1, 2, 3] - // sfixed64 value = 1 [(buf.validate.field).sfixed64 = { in: [1, 2, 3] }]; - // } - // ``` - repeated sfixed64 in = 6 [(predefined).cel = { - id: "sfixed64.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not be equal to any of the specified - // values. If the field value is one of the specified values, an error - // message is generated. - // - // ```proto - // message MySFixed64 { - // // value must not be in list [1, 2, 3] - // sfixed64 value = 1 [(buf.validate.field).sfixed64 = { not_in: [1, 2, 3] }]; - // } - // ``` - repeated sfixed64 not_in = 7 [(predefined).cel = { - id: "sfixed64.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MySFixed64 { - // sfixed64 value = 1 [ - // (buf.validate.field).sfixed64.example = 1, - // (buf.validate.field).sfixed64.example = 2 - // ]; - // } - // ``` - repeated sfixed64 example = 8 [(predefined).cel = { - id: "sfixed64.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// BoolRules describes the rules applied to `bool` values. These rules -// may also be applied to the `google.protobuf.BoolValue` Well-Known-Type. -message BoolRules { - // `const` requires the field value to exactly match the specified boolean value. - // If the field value doesn't match, an error message is generated. - // - // ```proto - // message MyBool { - // // value must equal true - // bool value = 1 [(buf.validate.field).bool.const = true]; - // } - // ``` - optional bool const = 1 [(predefined).cel = { - id: "bool.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyBool { - // bool value = 1 [ - // (buf.validate.field).bool.example = 1, - // (buf.validate.field).bool.example = 2 - // ]; - // } - // ``` - repeated bool example = 2 [(predefined).cel = { - id: "bool.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// StringRules describes the rules applied to `string` values These -// rules may also be applied to the `google.protobuf.StringValue` Well-Known-Type. -message StringRules { - // `const` requires the field value to exactly match the specified value. If - // the field value doesn't match, an error message is generated. - // - // ```proto - // message MyString { - // // value must equal `hello` - // string value = 1 [(buf.validate.field).string.const = "hello"]; - // } - // ``` - optional string const = 1 [(predefined).cel = { - id: "string.const" - expression: "this != getField(rules, 'const') ? 'must equal `%s`'.format([getField(rules, 'const')]) : ''" - }]; - - // `len` dictates that the field value must have the specified - // number of characters (Unicode code points), which may differ from the number - // of bytes in the string. If the field value does not meet the specified - // length, an error message will be generated. - // - // ```proto - // message MyString { - // // value length must be 5 characters - // string value = 1 [(buf.validate.field).string.len = 5]; - // } - // ``` - optional uint64 len = 19 [(predefined).cel = { - id: "string.len" - expression: "uint(this.size()) != rules.len ? 'must be %s characters'.format([rules.len]) : ''" - }]; - - // `min_len` specifies that the field value must have at least the specified - // number of characters (Unicode code points), which may differ from the number - // of bytes in the string. If the field value contains fewer characters, an error - // message will be generated. - // - // ```proto - // message MyString { - // // value length must be at least 3 characters - // string value = 1 [(buf.validate.field).string.min_len = 3]; - // } - // ``` - optional uint64 min_len = 2 [(predefined).cel = { - id: "string.min_len" - expression: "uint(this.size()) < rules.min_len ? 'must be at least %s characters'.format([rules.min_len]) : ''" - }]; - - // `max_len` specifies that the field value must have no more than the specified - // number of characters (Unicode code points), which may differ from the - // number of bytes in the string. If the field value contains more characters, - // an error message will be generated. - // - // ```proto - // message MyString { - // // value length must be at most 10 characters - // string value = 1 [(buf.validate.field).string.max_len = 10]; - // } - // ``` - optional uint64 max_len = 3 [(predefined).cel = { - id: "string.max_len" - expression: "uint(this.size()) > rules.max_len ? 'must be at most %s characters'.format([rules.max_len]) : ''" - }]; - - // `len_bytes` dictates that the field value must have the specified number of - // bytes. If the field value does not match the specified length in bytes, - // an error message will be generated. - // - // ```proto - // message MyString { - // // value length must be 6 bytes - // string value = 1 [(buf.validate.field).string.len_bytes = 6]; - // } - // ``` - optional uint64 len_bytes = 20 [(predefined).cel = { - id: "string.len_bytes" - expression: "uint(bytes(this).size()) != rules.len_bytes ? 'must be %s bytes'.format([rules.len_bytes]) : ''" - }]; - - // `min_bytes` specifies that the field value must have at least the specified - // number of bytes. If the field value contains fewer bytes, an error message - // will be generated. - // - // ```proto - // message MyString { - // // value length must be at least 4 bytes - // string value = 1 [(buf.validate.field).string.min_bytes = 4]; - // } - // - // ``` - optional uint64 min_bytes = 4 [(predefined).cel = { - id: "string.min_bytes" - expression: "uint(bytes(this).size()) < rules.min_bytes ? 'must be at least %s bytes'.format([rules.min_bytes]) : ''" - }]; - - // `max_bytes` specifies that the field value must have no more than the - // specified number of bytes. If the field value contains more bytes, an - // error message will be generated. - // - // ```proto - // message MyString { - // // value length must be at most 8 bytes - // string value = 1 [(buf.validate.field).string.max_bytes = 8]; - // } - // ``` - optional uint64 max_bytes = 5 [(predefined).cel = { - id: "string.max_bytes" - expression: "uint(bytes(this).size()) > rules.max_bytes ? 'must be at most %s bytes'.format([rules.max_bytes]) : ''" - }]; - - // `pattern` specifies that the field value must match the specified - // regular expression (RE2 syntax), with the expression provided without any - // delimiters. If the field value doesn't match the regular expression, an - // error message will be generated. - // - // ```proto - // message MyString { - // // value does not match regex pattern `^[a-zA-Z]//$` - // string value = 1 [(buf.validate.field).string.pattern = "^[a-zA-Z]//$"]; - // } - // ``` - optional string pattern = 6 [(predefined).cel = { - id: "string.pattern" - expression: "!this.matches(rules.pattern) ? 'does not match regex pattern `%s`'.format([rules.pattern]) : ''" - }]; - - // `prefix` specifies that the field value must have the - // specified substring at the beginning of the string. If the field value - // doesn't start with the specified prefix, an error message will be - // generated. - // - // ```proto - // message MyString { - // // value does not have prefix `pre` - // string value = 1 [(buf.validate.field).string.prefix = "pre"]; - // } - // ``` - optional string prefix = 7 [(predefined).cel = { - id: "string.prefix" - expression: "!this.startsWith(rules.prefix) ? 'does not have prefix `%s`'.format([rules.prefix]) : ''" - }]; - - // `suffix` specifies that the field value must have the - // specified substring at the end of the string. If the field value doesn't - // end with the specified suffix, an error message will be generated. - // - // ```proto - // message MyString { - // // value does not have suffix `post` - // string value = 1 [(buf.validate.field).string.suffix = "post"]; - // } - // ``` - optional string suffix = 8 [(predefined).cel = { - id: "string.suffix" - expression: "!this.endsWith(rules.suffix) ? 'does not have suffix `%s`'.format([rules.suffix]) : ''" - }]; - - // `contains` specifies that the field value must have the - // specified substring anywhere in the string. If the field value doesn't - // contain the specified substring, an error message will be generated. - // - // ```proto - // message MyString { - // // value does not contain substring `inside`. - // string value = 1 [(buf.validate.field).string.contains = "inside"]; - // } - // ``` - optional string contains = 9 [(predefined).cel = { - id: "string.contains" - expression: "!this.contains(rules.contains) ? 'does not contain substring `%s`'.format([rules.contains]) : ''" - }]; - - // `not_contains` specifies that the field value must not have the - // specified substring anywhere in the string. If the field value contains - // the specified substring, an error message will be generated. - // - // ```proto - // message MyString { - // // value contains substring `inside`. - // string value = 1 [(buf.validate.field).string.not_contains = "inside"]; - // } - // ``` - optional string not_contains = 23 [(predefined).cel = { - id: "string.not_contains" - expression: "this.contains(rules.not_contains) ? 'contains substring `%s`'.format([rules.not_contains]) : ''" - }]; - - // `in` specifies that the field value must be equal to one of the specified - // values. If the field value isn't one of the specified values, an error - // message will be generated. - // - // ```proto - // message MyString { - // // must be in list ["apple", "banana"] - // string value = 1 [(buf.validate.field).string.in = "apple", (buf.validate.field).string.in = "banana"]; - // } - // ``` - repeated string in = 10 [(predefined).cel = { - id: "string.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` specifies that the field value cannot be equal to any - // of the specified values. If the field value is one of the specified values, - // an error message will be generated. - // ```proto - // message MyString { - // // value must not be in list ["orange", "grape"] - // string value = 1 [(buf.validate.field).string.not_in = "orange", (buf.validate.field).string.not_in = "grape"]; - // } - // ``` - repeated string not_in = 11 [(predefined).cel = { - id: "string.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `WellKnown` rules provide advanced rules against common string - // patterns. - oneof well_known { - // `email` specifies that the field value must be a valid email address, for - // example "foo@example.com". - // - // Conforms to the definition for a valid email address from the [HTML standard](https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address). - // Note that this standard willfully deviates from [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322), - // which allows many unexpected forms of email addresses and will easily match - // a typographical error. - // - // If the field value isn't a valid email address, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid email address - // string value = 1 [(buf.validate.field).string.email = true]; - // } - // ``` - bool email = 12 [ - (predefined).cel = { - id: "string.email" - message: "must be a valid email address" - expression: "!rules.email || this == '' || this.isEmail()" - }, - (predefined).cel = { - id: "string.email_empty" - message: "value is empty, which is not a valid email address" - expression: "!rules.email || this != ''" - } - ]; - - // `hostname` specifies that the field value must be a valid hostname, for - // example "foo.example.com". - // - // A valid hostname follows the rules below: - // - The name consists of one or more labels, separated by a dot ("."). - // - Each label can be 1 to 63 alphanumeric characters. - // - A label can contain hyphens ("-"), but must not start or end with a hyphen. - // - The right-most label must not be digits only. - // - The name can have a trailing dot—for example, "foo.example.com.". - // - The name can be 253 characters at most, excluding the optional trailing dot. - // - // If the field value isn't a valid hostname, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid hostname - // string value = 1 [(buf.validate.field).string.hostname = true]; - // } - // ``` - bool hostname = 13 [ - (predefined).cel = { - id: "string.hostname" - message: "must be a valid hostname" - expression: "!rules.hostname || this == '' || this.isHostname()" - }, - (predefined).cel = { - id: "string.hostname_empty" - message: "value is empty, which is not a valid hostname" - expression: "!rules.hostname || this != ''" - } - ]; - - // `ip` specifies that the field value must be a valid IP (v4 or v6) address. - // - // IPv4 addresses are expected in the dotted decimal format—for example, "192.168.5.21". - // IPv6 addresses are expected in their text representation—for example, "::1", - // or "2001:0DB8:ABCD:0012::0". - // - // Both formats are well-defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). - // Zone identifiers for IPv6 addresses (for example, "fe80::a%en1") are supported. - // - // If the field value isn't a valid IP address, an error message will be - // generated. - // - // ```proto - // message MyString { - // // must be a valid IP address - // string value = 1 [(buf.validate.field).string.ip = true]; - // } - // ``` - bool ip = 14 [ - (predefined).cel = { - id: "string.ip" - message: "must be a valid IP address" - expression: "!rules.ip || this == '' || this.isIp()" - }, - (predefined).cel = { - id: "string.ip_empty" - message: "value is empty, which is not a valid IP address" - expression: "!rules.ip || this != ''" - } - ]; - - // `ipv4` specifies that the field value must be a valid IPv4 address—for - // example "192.168.5.21". If the field value isn't a valid IPv4 address, an - // error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid IPv4 address - // string value = 1 [(buf.validate.field).string.ipv4 = true]; - // } - // ``` - bool ipv4 = 15 [ - (predefined).cel = { - id: "string.ipv4" - message: "must be a valid IPv4 address" - expression: "!rules.ipv4 || this == '' || this.isIp(4)" - }, - (predefined).cel = { - id: "string.ipv4_empty" - message: "value is empty, which is not a valid IPv4 address" - expression: "!rules.ipv4 || this != ''" - } - ]; - - // `ipv6` specifies that the field value must be a valid IPv6 address—for - // example "::1", or "d7a:115c:a1e0:ab12:4843:cd96:626b:430b". If the field - // value is not a valid IPv6 address, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid IPv6 address - // string value = 1 [(buf.validate.field).string.ipv6 = true]; - // } - // ``` - bool ipv6 = 16 [ - (predefined).cel = { - id: "string.ipv6" - message: "must be a valid IPv6 address" - expression: "!rules.ipv6 || this == '' || this.isIp(6)" - }, - (predefined).cel = { - id: "string.ipv6_empty" - message: "value is empty, which is not a valid IPv6 address" - expression: "!rules.ipv6 || this != ''" - } - ]; - - // `uri` specifies that the field value must be a valid URI, for example - // "https://example.com/foo/bar?baz=quux#frag". - // - // URI is defined in the internet standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). - // Zone Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)). - // - // If the field value isn't a valid URI, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid URI - // string value = 1 [(buf.validate.field).string.uri = true]; - // } - // ``` - bool uri = 17 [ - (predefined).cel = { - id: "string.uri" - message: "must be a valid URI" - expression: "!rules.uri || this == '' || this.isUri()" - }, - (predefined).cel = { - id: "string.uri_empty" - message: "value is empty, which is not a valid URI" - expression: "!rules.uri || this != ''" - } - ]; - - // `uri_ref` specifies that the field value must be a valid URI Reference—either - // a URI such as "https://example.com/foo/bar?baz=quux#frag", or a Relative - // Reference such as "./foo/bar?query". - // - // URI, URI Reference, and Relative Reference are defined in the internet - // standard [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). Zone - // Identifiers in IPv6 address literals are supported ([RFC 6874](https://datatracker.ietf.org/doc/html/rfc6874)). - // - // If the field value isn't a valid URI Reference, an error message will be - // generated. - // - // ```proto - // message MyString { - // // must be a valid URI Reference - // string value = 1 [(buf.validate.field).string.uri_ref = true]; - // } - // ``` - bool uri_ref = 18 [(predefined).cel = { - id: "string.uri_ref" - message: "must be a valid URI Reference" - expression: "!rules.uri_ref || this.isUriRef()" - }]; - - // `address` specifies that the field value must be either a valid hostname - // (for example, "example.com"), or a valid IP (v4 or v6) address (for example, - // "192.168.0.1", or "::1"). If the field value isn't a valid hostname or IP, - // an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid hostname, or ip address - // string value = 1 [(buf.validate.field).string.address = true]; - // } - // ``` - bool address = 21 [ - (predefined).cel = { - id: "string.address" - message: "must be a valid hostname, or ip address" - expression: "!rules.address || this == '' || this.isHostname() || this.isIp()" - }, - (predefined).cel = { - id: "string.address_empty" - message: "value is empty, which is not a valid hostname, or ip address" - expression: "!rules.address || this != ''" - } - ]; - - // `uuid` specifies that the field value must be a valid UUID as defined by - // [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). If the - // field value isn't a valid UUID, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid UUID - // string value = 1 [(buf.validate.field).string.uuid = true]; - // } - // ``` - bool uuid = 22 [ - (predefined).cel = { - id: "string.uuid" - message: "must be a valid UUID" - expression: "!rules.uuid || this == '' || this.matches('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')" - }, - (predefined).cel = { - id: "string.uuid_empty" - message: "value is empty, which is not a valid UUID" - expression: "!rules.uuid || this != ''" - } - ]; - - // `tuuid` (trimmed UUID) specifies that the field value must be a valid UUID as - // defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2) with all dashes - // omitted. If the field value isn't a valid UUID without dashes, an error message - // will be generated. - // - // ```proto - // message MyString { - // // must be a valid trimmed UUID - // string value = 1 [(buf.validate.field).string.tuuid = true]; - // } - // ``` - bool tuuid = 33 [ - (predefined).cel = { - id: "string.tuuid" - message: "must be a valid trimmed UUID" - expression: "!rules.tuuid || this == '' || this.matches('^[0-9a-fA-F]{32}$')" - }, - (predefined).cel = { - id: "string.tuuid_empty" - message: "value is empty, which is not a valid trimmed UUID" - expression: "!rules.tuuid || this != ''" - } - ]; - - // `ip_with_prefixlen` specifies that the field value must be a valid IP - // (v4 or v6) address with prefix length—for example, "192.168.5.21/16" or - // "2001:0DB8:ABCD:0012::F1/64". If the field value isn't a valid IP with - // prefix length, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid IP with prefix length - // string value = 1 [(buf.validate.field).string.ip_with_prefixlen = true]; - // } - // ``` - bool ip_with_prefixlen = 26 [ - (predefined).cel = { - id: "string.ip_with_prefixlen" - message: "must be a valid IP prefix" - expression: "!rules.ip_with_prefixlen || this == '' || this.isIpPrefix()" - }, - (predefined).cel = { - id: "string.ip_with_prefixlen_empty" - message: "value is empty, which is not a valid IP prefix" - expression: "!rules.ip_with_prefixlen || this != ''" - } - ]; - - // `ipv4_with_prefixlen` specifies that the field value must be a valid - // IPv4 address with prefix length—for example, "192.168.5.21/16". If the - // field value isn't a valid IPv4 address with prefix length, an error - // message will be generated. - // - // ```proto - // message MyString { - // // must be a valid IPv4 address with prefix length - // string value = 1 [(buf.validate.field).string.ipv4_with_prefixlen = true]; - // } - // ``` - bool ipv4_with_prefixlen = 27 [ - (predefined).cel = { - id: "string.ipv4_with_prefixlen" - message: "must be a valid IPv4 address with prefix length" - expression: "!rules.ipv4_with_prefixlen || this == '' || this.isIpPrefix(4)" - }, - (predefined).cel = { - id: "string.ipv4_with_prefixlen_empty" - message: "value is empty, which is not a valid IPv4 address with prefix length" - expression: "!rules.ipv4_with_prefixlen || this != ''" - } - ]; - - // `ipv6_with_prefixlen` specifies that the field value must be a valid - // IPv6 address with prefix length—for example, "2001:0DB8:ABCD:0012::F1/64". - // If the field value is not a valid IPv6 address with prefix length, - // an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid IPv6 address prefix length - // string value = 1 [(buf.validate.field).string.ipv6_with_prefixlen = true]; - // } - // ``` - bool ipv6_with_prefixlen = 28 [ - (predefined).cel = { - id: "string.ipv6_with_prefixlen" - message: "must be a valid IPv6 address with prefix length" - expression: "!rules.ipv6_with_prefixlen || this == '' || this.isIpPrefix(6)" - }, - (predefined).cel = { - id: "string.ipv6_with_prefixlen_empty" - message: "value is empty, which is not a valid IPv6 address with prefix length" - expression: "!rules.ipv6_with_prefixlen || this != ''" - } - ]; - - // `ip_prefix` specifies that the field value must be a valid IP (v4 or v6) - // prefix—for example, "192.168.0.0/16" or "2001:0DB8:ABCD:0012::0/64". - // - // The prefix must have all zeros for the unmasked bits. For example, - // "2001:0DB8:ABCD:0012::0/64" designates the left-most 64 bits for the - // prefix, and the remaining 64 bits must be zero. - // - // If the field value isn't a valid IP prefix, an error message will be - // generated. - // - // ```proto - // message MyString { - // // must be a valid IP prefix - // string value = 1 [(buf.validate.field).string.ip_prefix = true]; - // } - // ``` - bool ip_prefix = 29 [ - (predefined).cel = { - id: "string.ip_prefix" - message: "must be a valid IP prefix" - expression: "!rules.ip_prefix || this == '' || this.isIpPrefix(true)" - }, - (predefined).cel = { - id: "string.ip_prefix_empty" - message: "value is empty, which is not a valid IP prefix" - expression: "!rules.ip_prefix || this != ''" - } - ]; - - // `ipv4_prefix` specifies that the field value must be a valid IPv4 - // prefix, for example "192.168.0.0/16". - // - // The prefix must have all zeros for the unmasked bits. For example, - // "192.168.0.0/16" designates the left-most 16 bits for the prefix, - // and the remaining 16 bits must be zero. - // - // If the field value isn't a valid IPv4 prefix, an error message - // will be generated. - // - // ```proto - // message MyString { - // // must be a valid IPv4 prefix - // string value = 1 [(buf.validate.field).string.ipv4_prefix = true]; - // } - // ``` - bool ipv4_prefix = 30 [ - (predefined).cel = { - id: "string.ipv4_prefix" - message: "must be a valid IPv4 prefix" - expression: "!rules.ipv4_prefix || this == '' || this.isIpPrefix(4, true)" - }, - (predefined).cel = { - id: "string.ipv4_prefix_empty" - message: "value is empty, which is not a valid IPv4 prefix" - expression: "!rules.ipv4_prefix || this != ''" - } - ]; - - // `ipv6_prefix` specifies that the field value must be a valid IPv6 prefix—for - // example, "2001:0DB8:ABCD:0012::0/64". - // - // The prefix must have all zeros for the unmasked bits. For example, - // "2001:0DB8:ABCD:0012::0/64" designates the left-most 64 bits for the - // prefix, and the remaining 64 bits must be zero. - // - // If the field value is not a valid IPv6 prefix, an error message will be - // generated. - // - // ```proto - // message MyString { - // // must be a valid IPv6 prefix - // string value = 1 [(buf.validate.field).string.ipv6_prefix = true]; - // } - // ``` - bool ipv6_prefix = 31 [ - (predefined).cel = { - id: "string.ipv6_prefix" - message: "must be a valid IPv6 prefix" - expression: "!rules.ipv6_prefix || this == '' || this.isIpPrefix(6, true)" - }, - (predefined).cel = { - id: "string.ipv6_prefix_empty" - message: "value is empty, which is not a valid IPv6 prefix" - expression: "!rules.ipv6_prefix || this != ''" - } - ]; - - // `host_and_port` specifies that the field value must be a valid host/port - // pair—for example, "example.com:8080". - // - // The host can be one of: - // - An IPv4 address in dotted decimal format—for example, "192.168.5.21". - // - An IPv6 address enclosed in square brackets—for example, "[2001:0DB8:ABCD:0012::F1]". - // - A hostname—for example, "example.com". - // - // The port is separated by a colon. It must be non-empty, with a decimal number - // in the range of 0-65535, inclusive. - bool host_and_port = 32 [ - (predefined).cel = { - id: "string.host_and_port" - message: "must be a valid host (hostname or IP address) and port pair" - expression: "!rules.host_and_port || this == '' || this.isHostAndPort(true)" - }, - (predefined).cel = { - id: "string.host_and_port_empty" - message: "value is empty, which is not a valid host and port pair" - expression: "!rules.host_and_port || this != ''" - } - ]; - - // `ulid` specifies that the field value must be a valid ULID (Universally Unique - // Lexicographically Sortable Identifier) as defined by the [ULID specification](https://github.com/ulid/spec). - // If the field value isn't a valid ULID, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid ULID - // string value = 1 [(buf.validate.field).string.ulid = true]; - // } - // ``` - bool ulid = 35 [ - (predefined).cel = { - id: "string.ulid" - message: "must be a valid ULID" - expression: "!rules.ulid || this == '' || this.matches('^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$')" - }, - (predefined).cel = { - id: "string.ulid_empty" - message: "value is empty, which is not a valid ULID" - expression: "!rules.ulid || this != ''" - } - ]; - - // `protobuf_fqn` specifies that the field value must be a valid fully-qualified - // Protobuf name as defined by the [Protobuf Language Specification](https://protobuf.com/docs/language-spec). - // - // A fully-qualified Protobuf name is a dot-separated list of Protobuf identifiers, - // where each identifier starts with a letter or underscore and is followed by zero or - // more letters, underscores, or digits. - // - // Examples: "buf.validate", "google.protobuf.Timestamp", "my_package.MyMessage". - // - // Note: historically, fully-qualified Protobuf names were represented with a leading - // dot (for example, ".buf.validate.StringRules"). Modern Protobuf does not use the - // leading dot, and most fully-qualified names are represented without it. Use - // `protobuf_dot_fqn` if a leading dot is required. - // - // If the field value isn't a valid fully-qualified Protobuf name, an error message - // will be generated. - // - // ```proto - // message MyString { - // // value must be a valid fully-qualified Protobuf name - // string value = 1 [(buf.validate.field).string.protobuf_fqn = true]; - // } - // ``` - bool protobuf_fqn = 37 [ - (predefined).cel = { - id: "string.protobuf_fqn" - message: "must be a valid fully-qualified Protobuf name" - expression: "!rules.protobuf_fqn || this == '' || this.matches('^[A-Za-z_][A-Za-z_0-9]*(\\\\.[A-Za-z_][A-Za-z_0-9]*)*$')" - }, - (predefined).cel = { - id: "string.protobuf_fqn_empty" - message: "value is empty, which is not a valid fully-qualified Protobuf name" - expression: "!rules.protobuf_fqn || this != ''" - } - ]; - - // `protobuf_dot_fqn` specifies that the field value must be a valid fully-qualified - // Protobuf name with a leading dot, as defined by the - // [Protobuf Language Specification](https://protobuf.com/docs/language-spec). - // - // A fully-qualified Protobuf name with a leading dot is a dot followed by a - // dot-separated list of Protobuf identifiers, where each identifier starts with a - // letter or underscore and is followed by zero or more letters, underscores, or - // digits. - // - // Examples: ".buf.validate", ".google.protobuf.Timestamp", ".my_package.MyMessage". - // - // Note: this is the historical representation of fully-qualified Protobuf names, - // where a leading dot denotes an absolute reference. Modern Protobuf does not use - // the leading dot, and most fully-qualified names are represented without it. Most - // users will want to use `protobuf_fqn` instead. - // - // If the field value isn't a valid fully-qualified Protobuf name with a leading dot, - // an error message will be generated. - // - // ```proto - // message MyString { - // // value must be a valid fully-qualified Protobuf name with a leading dot - // string value = 1 [(buf.validate.field).string.protobuf_dot_fqn = true]; - // } - // ``` - bool protobuf_dot_fqn = 38 [ - (predefined).cel = { - id: "string.protobuf_dot_fqn" - message: "must be a valid fully-qualified Protobuf name with a leading dot" - expression: "!rules.protobuf_dot_fqn || this == '' || this.matches('^\\\\.[A-Za-z_][A-Za-z_0-9]*(\\\\.[A-Za-z_][A-Za-z_0-9]*)*$')" - }, - (predefined).cel = { - id: "string.protobuf_dot_fqn_empty" - message: "value is empty, which is not a valid fully-qualified Protobuf name with a leading dot" - expression: "!rules.protobuf_dot_fqn || this != ''" - } - ]; - - // `well_known_regex` specifies a common well-known pattern - // defined as a regex. If the field value doesn't match the well-known - // regex, an error message will be generated. - // - // ```proto - // message MyString { - // // must be a valid HTTP header value - // string value = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE]; - // } - // ``` - // - // #### KnownRegex - // - // `well_known_regex` contains some well-known patterns. - // - // | Name | Number | Description | - // |-------------------------------|--------|-------------------------------------------| - // | KNOWN_REGEX_UNSPECIFIED | 0 | | - // | KNOWN_REGEX_HTTP_HEADER_NAME | 1 | HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2) | - // | KNOWN_REGEX_HTTP_HEADER_VALUE | 2 | HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4) | - KnownRegex well_known_regex = 24 [ - (predefined).cel = { - id: "string.well_known_regex.header_name" - message: "must be a valid HTTP header name" - expression: - "rules.well_known_regex != 1 || this == '' || this.matches(!has(rules.strict) || rules.strict ?" - "'^:?[0-9a-zA-Z!#$%&\\'*+-.^_|~\\x60]+$' :" - "'^[^\\u0000\\u000A\\u000D]+$')" - }, - (predefined).cel = { - id: "string.well_known_regex.header_name_empty" - message: "value is empty, which is not a valid HTTP header name" - expression: "rules.well_known_regex != 1 || this != ''" - }, - (predefined).cel = { - id: "string.well_known_regex.header_value" - message: "must be a valid HTTP header value" - expression: - "rules.well_known_regex != 2 || this.matches(!has(rules.strict) || rules.strict ?" - "'^[^\\u0000-\\u0008\\u000A-\\u001F\\u007F]*$' :" - "'^[^\\u0000\\u000A\\u000D]*$')" - } - ]; - } - - // This applies to regexes `HTTP_HEADER_NAME` and `HTTP_HEADER_VALUE` to - // enable strict header validation. By default, this is true, and HTTP header - // validations are [RFC-compliant](https://datatracker.ietf.org/doc/html/rfc7230#section-3). Setting to false will enable looser - // validations that only disallow `\r\n\0` characters, which can be used to - // bypass header matching rules. - // - // ```proto - // message MyString { - // // The field `value` must have be a valid HTTP headers, but not enforced with strict rules. - // string value = 1 [(buf.validate.field).string.strict = false]; - // } - // ``` - optional bool strict = 25; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyString { - // string value = 1 [ - // (buf.validate.field).string.example = "hello", - // (buf.validate.field).string.example = "world" - // ]; - // } - // ``` - repeated string example = 34 [(predefined).cel = { - id: "string.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// KnownRegex contains some well-known patterns. -enum KnownRegex { - KNOWN_REGEX_UNSPECIFIED = 0; - - // HTTP header name as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2). - KNOWN_REGEX_HTTP_HEADER_NAME = 1; - - // HTTP header value as defined by [RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4). - KNOWN_REGEX_HTTP_HEADER_VALUE = 2; -} - -// BytesRules describe the rules applied to `bytes` values. These rules -// may also be applied to the `google.protobuf.BytesValue` Well-Known-Type. -message BytesRules { - // `const` requires the field value to exactly match the specified bytes - // value. If the field value doesn't match, an error message is generated. - // - // ```proto - // message MyBytes { - // // must be "\x01\x02\x03\x04" - // bytes value = 1 [(buf.validate.field).bytes.const = "\x01\x02\x03\x04"]; - // } - // ``` - optional bytes const = 1 [(predefined).cel = { - id: "bytes.const" - expression: "this != getField(rules, 'const') ? 'must be %x'.format([getField(rules, 'const')]) : ''" - }]; - - // `len` requires the field value to have the specified length in bytes. - // If the field value doesn't match, an error message is generated. - // - // ```proto - // message MyBytes { - // // value length must be 4 bytes. - // optional bytes value = 1 [(buf.validate.field).bytes.len = 4]; - // } - // ``` - optional uint64 len = 13 [(predefined).cel = { - id: "bytes.len" - expression: "uint(this.size()) != rules.len ? 'must be %s bytes'.format([rules.len]) : ''" - }]; - - // `min_len` requires the field value to have at least the specified minimum - // length in bytes. - // If the field value doesn't meet the requirement, an error message is generated. - // - // ```proto - // message MyBytes { - // // value length must be at least 2 bytes. - // optional bytes value = 1 [(buf.validate.field).bytes.min_len = 2]; - // } - // ``` - optional uint64 min_len = 2 [(predefined).cel = { - id: "bytes.min_len" - expression: "uint(this.size()) < rules.min_len ? 'must be at least %s bytes'.format([rules.min_len]) : ''" - }]; - - // `max_len` requires the field value to have at most the specified maximum - // length in bytes. - // If the field value exceeds the requirement, an error message is generated. - // - // ```proto - // message MyBytes { - // // must be at most 6 bytes. - // optional bytes value = 1 [(buf.validate.field).bytes.max_len = 6]; - // } - // ``` - optional uint64 max_len = 3 [(predefined).cel = { - id: "bytes.max_len" - expression: "uint(this.size()) > rules.max_len ? 'must be at most %s bytes'.format([rules.max_len]) : ''" - }]; - - // `pattern` requires the field value to match the specified regular - // expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)). - // The value of the field must be valid UTF-8 or validation will fail with a - // runtime error. - // If the field value doesn't match the pattern, an error message is generated. - // - // ```proto - // message MyBytes { - // // value must match regex pattern "^[a-zA-Z0-9]+$". - // optional bytes value = 1 [(buf.validate.field).bytes.pattern = "^[a-zA-Z0-9]+$"]; - // } - // ``` - optional string pattern = 4 [(predefined).cel = { - id: "bytes.pattern" - expression: "!string(this).matches(rules.pattern) ? 'must match regex pattern `%s`'.format([rules.pattern]) : ''" - }]; - - // `prefix` requires the field value to have the specified bytes at the - // beginning of the string. - // If the field value doesn't meet the requirement, an error message is generated. - // - // ```proto - // message MyBytes { - // // value does not have prefix \x01\x02 - // optional bytes value = 1 [(buf.validate.field).bytes.prefix = "\x01\x02"]; - // } - // ``` - optional bytes prefix = 5 [(predefined).cel = { - id: "bytes.prefix" - expression: "!this.startsWith(rules.prefix) ? 'does not have prefix %x'.format([rules.prefix]) : ''" - }]; - - // `suffix` requires the field value to have the specified bytes at the end - // of the string. - // If the field value doesn't meet the requirement, an error message is generated. - // - // ```proto - // message MyBytes { - // // value does not have suffix \x03\x04 - // optional bytes value = 1 [(buf.validate.field).bytes.suffix = "\x03\x04"]; - // } - // ``` - optional bytes suffix = 6 [(predefined).cel = { - id: "bytes.suffix" - expression: "!this.endsWith(rules.suffix) ? 'does not have suffix %x'.format([rules.suffix]) : ''" - }]; - - // `contains` requires the field value to have the specified bytes anywhere in - // the string. - // If the field value doesn't meet the requirement, an error message is generated. - // - // ```proto - // message MyBytes { - // // value does not contain \x02\x03 - // optional bytes value = 1 [(buf.validate.field).bytes.contains = "\x02\x03"]; - // } - // ``` - optional bytes contains = 7 [(predefined).cel = { - id: "bytes.contains" - expression: "!this.contains(rules.contains) ? 'does not contain %x'.format([rules.contains]) : ''" - }]; - - // `in` requires the field value to be equal to one of the specified - // values. If the field value doesn't match any of the specified values, an - // error message is generated. - // - // ```proto - // message MyBytes { - // // value must in ["\x01\x02", "\x02\x03", "\x03\x04"] - // optional bytes value = 1 [(buf.validate.field).bytes.in = {"\x01\x02", "\x02\x03", "\x03\x04"}]; - // } - // ``` - repeated bytes in = 8 [(predefined).cel = { - id: "bytes.in" - expression: "getField(rules, 'in').size() > 0 && !(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to be not equal to any of the specified - // values. - // If the field value matches any of the specified values, an error message is - // generated. - // - // ```proto - // message MyBytes { - // // value must not in ["\x01\x02", "\x02\x03", "\x03\x04"] - // optional bytes value = 1 [(buf.validate.field).bytes.not_in = {"\x01\x02", "\x02\x03", "\x03\x04"}]; - // } - // ``` - repeated bytes not_in = 9 [(predefined).cel = { - id: "bytes.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // WellKnown rules provide advanced rules against common byte - // patterns - oneof well_known { - // `ip` ensures that the field `value` is a valid IP address (v4 or v6) in byte format. - // If the field value doesn't meet this rule, an error message is generated. - // - // ```proto - // message MyBytes { - // // must be a valid IP address - // optional bytes value = 1 [(buf.validate.field).bytes.ip = true]; - // } - // ``` - bool ip = 10 [ - (predefined).cel = { - id: "bytes.ip" - message: "must be a valid IP address" - expression: "!rules.ip || this.size() == 0 || this.size() == 4 || this.size() == 16" - }, - (predefined).cel = { - id: "bytes.ip_empty" - message: "value is empty, which is not a valid IP address" - expression: "!rules.ip || this.size() != 0" - } - ]; - - // `ipv4` ensures that the field `value` is a valid IPv4 address in byte format. - // If the field value doesn't meet this rule, an error message is generated. - // - // ```proto - // message MyBytes { - // // must be a valid IPv4 address - // optional bytes value = 1 [(buf.validate.field).bytes.ipv4 = true]; - // } - // ``` - bool ipv4 = 11 [ - (predefined).cel = { - id: "bytes.ipv4" - message: "must be a valid IPv4 address" - expression: "!rules.ipv4 || this.size() == 0 || this.size() == 4" - }, - (predefined).cel = { - id: "bytes.ipv4_empty" - message: "value is empty, which is not a valid IPv4 address" - expression: "!rules.ipv4 || this.size() != 0" - } - ]; - - // `ipv6` ensures that the field `value` is a valid IPv6 address in byte format. - // If the field value doesn't meet this rule, an error message is generated. - // ```proto - // message MyBytes { - // // must be a valid IPv6 address - // optional bytes value = 1 [(buf.validate.field).bytes.ipv6 = true]; - // } - // ``` - bool ipv6 = 12 [ - (predefined).cel = { - id: "bytes.ipv6" - message: "must be a valid IPv6 address" - expression: "!rules.ipv6 || this.size() == 0 || this.size() == 16" - }, - (predefined).cel = { - id: "bytes.ipv6_empty" - message: "value is empty, which is not a valid IPv6 address" - expression: "!rules.ipv6 || this.size() != 0" - } - ]; - - // `uuid` ensures that the field value encodes 128-bit UUID data as defined - // by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2). - // The field must contain exactly 16 bytes representing the UUID. If the - // field value isn't a valid UUID, an error message will be generated. - // - // ```proto - // message MyBytes { - // // must be a valid UUID - // optional bytes value = 1 [(buf.validate.field).bytes.uuid = true]; - // } - // ``` - bool uuid = 15 [ - (predefined).cel = { - id: "bytes.uuid" - message: "must be a valid UUID" - expression: "!rules.uuid || this.size() == 0 || this.size() == 16" - }, - (predefined).cel = { - id: "bytes.uuid_empty" - message: "value is empty, which is not a valid UUID" - expression: "!rules.uuid || this.size() != 0" - } - ]; - } - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyBytes { - // bytes value = 1 [ - // (buf.validate.field).bytes.example = "\x01\x02", - // (buf.validate.field).bytes.example = "\x02\x03" - // ]; - // } - // ``` - repeated bytes example = 14 [(predefined).cel = { - id: "bytes.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// EnumRules describe the rules applied to `enum` values. -message EnumRules { - // `const` requires the field value to exactly match the specified enum value. - // If the field value doesn't match, an error message is generated. - // - // ```proto - // enum MyEnum { - // MY_ENUM_UNSPECIFIED = 0; - // MY_ENUM_VALUE1 = 1; - // MY_ENUM_VALUE2 = 2; - // } - // - // message MyMessage { - // // The field `value` must be exactly MY_ENUM_VALUE1. - // MyEnum value = 1 [(buf.validate.field).enum.const = 1]; - // } - // ``` - optional int32 const = 1 [(predefined).cel = { - id: "enum.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - - // `defined_only` requires the field value to be one of the defined values for - // this enum, failing on any undefined value. - // - // ```proto - // enum MyEnum { - // MY_ENUM_UNSPECIFIED = 0; - // MY_ENUM_VALUE1 = 1; - // MY_ENUM_VALUE2 = 2; - // } - // - // message MyMessage { - // // The field `value` must be a defined value of MyEnum. - // MyEnum value = 1 [(buf.validate.field).enum.defined_only = true]; - // } - // ``` - optional bool defined_only = 2; - - // `in` requires the field value to be equal to one of the - // specified enum values. If the field value doesn't match any of the - // specified values, an error message is generated. - // - // ```proto - // enum MyEnum { - // MY_ENUM_UNSPECIFIED = 0; - // MY_ENUM_VALUE1 = 1; - // MY_ENUM_VALUE2 = 2; - // } - // - // message MyMessage { - // // The field `value` must be equal to one of the specified values. - // MyEnum value = 1 [(buf.validate.field).enum = { in: [1, 2]}]; - // } - // ``` - repeated int32 in = 3 [(predefined).cel = { - id: "enum.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to be not equal to any of the - // specified enum values. If the field value matches one of the specified - // values, an error message is generated. - // - // ```proto - // enum MyEnum { - // MY_ENUM_UNSPECIFIED = 0; - // MY_ENUM_VALUE1 = 1; - // MY_ENUM_VALUE2 = 2; - // } - // - // message MyMessage { - // // The field `value` must not be equal to any of the specified values. - // MyEnum value = 1 [(buf.validate.field).enum = { not_in: [1, 2]}]; - // } - // ``` - repeated int32 not_in = 4 [(predefined).cel = { - id: "enum.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // enum MyEnum { - // MY_ENUM_UNSPECIFIED = 0; - // MY_ENUM_VALUE1 = 1; - // MY_ENUM_VALUE2 = 2; - // } - // - // message MyMessage { - // (buf.validate.field).enum.example = 1, - // (buf.validate.field).enum.example = 2 - // } - // ``` - repeated int32 example = 5 [(predefined).cel = { - id: "enum.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// RepeatedRules describe the rules applied to `repeated` values. -message RepeatedRules { - // `min_items` requires that this field must contain at least the specified - // minimum number of items. - // - // Note that `min_items = 1` is equivalent to setting a field as `required`. - // - // ```proto - // message MyRepeated { - // // value must contain at least 2 items - // repeated string value = 1 [(buf.validate.field).repeated.min_items = 2]; - // } - // ``` - optional uint64 min_items = 1 [(predefined).cel = { - id: "repeated.min_items" - expression: "uint(this.size()) < rules.min_items ? 'must contain at least %d item(s)'.format([rules.min_items]) : ''" - }]; - - // `max_items` denotes that this field must not exceed a - // certain number of items as the upper limit. If the field contains more - // items than specified, an error message will be generated, requiring the - // field to maintain no more than the specified number of items. - // - // ```proto - // message MyRepeated { - // // value must contain no more than 3 item(s) - // repeated string value = 1 [(buf.validate.field).repeated.max_items = 3]; - // } - // ``` - optional uint64 max_items = 2 [(predefined).cel = { - id: "repeated.max_items" - expression: "uint(this.size()) > rules.max_items ? 'must contain no more than %s item(s)'.format([rules.max_items]) : ''" - }]; - - // `unique` indicates that all elements in this field must - // be unique. This rule is strictly applicable to scalar and enum - // types, with message types not being supported. - // - // ```proto - // message MyRepeated { - // // repeated value must contain unique items - // repeated string value = 1 [(buf.validate.field).repeated.unique = true]; - // } - // ``` - optional bool unique = 3 [(predefined).cel = { - id: "repeated.unique" - message: "repeated value must contain unique items" - expression: "!rules.unique || this.unique()" - }]; - - // `items` details the rules to be applied to each item - // in the field. Even for repeated message fields, validation is executed - // against each item unless `ignore` is specified. - // - // ```proto - // message MyRepeated { - // // The items in the field `value` must follow the specified rules. - // repeated string value = 1 [(buf.validate.field).repeated.items = { - // string: { - // min_len: 3 - // max_len: 10 - // } - // }]; - // } - // ``` - // - // Note that the `required` rule does not apply. Repeated items - // cannot be unset. - optional FieldRules items = 4; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// MapRules describe the rules applied to `map` values. -message MapRules { - // Specifies the minimum number of key-value pairs allowed. If the field has - // fewer key-value pairs than specified, an error message is generated. - // - // ```proto - // message MyMap { - // // The field `value` must have at least 2 key-value pairs. - // map value = 1 [(buf.validate.field).map.min_pairs = 2]; - // } - // ``` - optional uint64 min_pairs = 1 [(predefined).cel = { - id: "map.min_pairs" - expression: "uint(this.size()) < rules.min_pairs ? 'map must be at least %d entries'.format([rules.min_pairs]) : ''" - }]; - - // Specifies the maximum number of key-value pairs allowed. If the field has - // more key-value pairs than specified, an error message is generated. - // - // ```proto - // message MyMap { - // // The field `value` must have at most 3 key-value pairs. - // map value = 1 [(buf.validate.field).map.max_pairs = 3]; - // } - // ``` - optional uint64 max_pairs = 2 [(predefined).cel = { - id: "map.max_pairs" - expression: "uint(this.size()) > rules.max_pairs ? 'map must be at most %d entries'.format([rules.max_pairs]) : ''" - }]; - - // Specifies the rules to be applied to each key in the field. - // - // ```proto - // message MyMap { - // // The keys in the field `value` must follow the specified rules. - // map value = 1 [(buf.validate.field).map.keys = { - // string: { - // min_len: 3 - // max_len: 10 - // } - // }]; - // } - // ``` - // - // Note that the `required` rule does not apply. Map keys cannot be unset. - optional FieldRules keys = 4; - - // Specifies the rules to be applied to the value of each key in the - // field. Message values will still have their validations evaluated unless - // `ignore` is specified. - // - // ```proto - // message MyMap { - // // The values in the field `value` must follow the specified rules. - // map value = 1 [(buf.validate.field).map.values = { - // string: { - // min_len: 5 - // max_len: 20 - // } - // }]; - // } - // ``` - // Note that the `required` rule does not apply. Map values cannot be unset. - optional FieldRules values = 5; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// AnyRules describe rules applied exclusively to the `google.protobuf.Any` well-known type. -message AnyRules { - // `in` requires the field's `type_url` to be equal to one of the - // specified values. If it doesn't match any of the specified values, an error - // message is generated. - // - // ```proto - // message MyAny { - // // The `value` field must have a `type_url` equal to one of the specified values. - // google.protobuf.Any value = 1 [(buf.validate.field).any = { - // in: ["type.googleapis.com/MyType1", "type.googleapis.com/MyType2"] - // }]; - // } - // ``` - repeated string in = 2; - - // `not_in` requires the field's type_url to be not equal to any of the specified values. If it matches any of the specified values, an error message is generated. - // - // ```proto - // message MyAny { - // // The `value` field must not have a `type_url` equal to any of the specified values. - // google.protobuf.Any value = 1 [(buf.validate.field).any = { - // not_in: ["type.googleapis.com/ForbiddenType1", "type.googleapis.com/ForbiddenType2"] - // }]; - // } - // ``` - repeated string not_in = 3; -} - -// DurationRules describe the rules applied exclusively to the `google.protobuf.Duration` well-known type. -message DurationRules { - // `const` dictates that the field must match the specified value of the `google.protobuf.Duration` type exactly. - // If the field's value deviates from the specified value, an error message - // will be generated. - // - // ```proto - // message MyDuration { - // // value must equal 5s - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.const = "5s"]; - // } - // ``` - optional google.protobuf.Duration const = 2 [(predefined).cel = { - id: "duration.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` stipulates that the field must be less than the specified value of the `google.protobuf.Duration` type, - // exclusive. If the field's value is greater than or equal to the specified - // value, an error message will be generated. - // - // ```proto - // message MyDuration { - // // must be less than 5s - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.lt = "5s"]; - // } - // ``` - google.protobuf.Duration lt = 3 [(predefined).cel = { - id: "duration.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` indicates that the field must be less than or equal to the specified - // value of the `google.protobuf.Duration` type, inclusive. If the field's value is greater than the specified value, - // an error message will be generated. - // - // ```proto - // message MyDuration { - // // must be less than or equal to 10s - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.lte = "10s"]; - // } - // ``` - google.protobuf.Duration lte = 4 [(predefined).cel = { - id: "duration.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - } - oneof greater_than { - // `gt` requires the duration field value to be greater than the specified - // value (exclusive). If the value of `gt` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyDuration { - // // duration must be greater than 5s [duration.gt] - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.gt = { seconds: 5 }]; - // - // // duration must be greater than 5s and less than 10s [duration.gt_lt] - // google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gt: { seconds: 5 }, lt: { seconds: 10 } }]; - // - // // duration must be greater than 10s or less than 5s [duration.gt_lt_exclusive] - // google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gt: { seconds: 10 }, lt: { seconds: 5 } }]; - // } - // ``` - google.protobuf.Duration gt = 5 [ - (predefined).cel = { - id: "duration.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "duration.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "duration.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "duration.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "duration.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the duration field value to be greater than or equal to the - // specified value (exclusive). If the value of `gte` is larger than a - // specified `lt` or `lte`, the range is reversed, and the field value must - // be outside the specified range. If the field value doesn't meet the - // required conditions, an error message is generated. - // - // ```proto - // message MyDuration { - // // duration must be greater than or equal to 5s [duration.gte] - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.gte = { seconds: 5 }]; - // - // // duration must be greater than or equal to 5s and less than 10s [duration.gte_lt] - // google.protobuf.Duration another_value = 2 [(buf.validate.field).duration = { gte: { seconds: 5 }, lt: { seconds: 10 } }]; - // - // // duration must be greater than or equal to 10s or less than 5s [duration.gte_lt_exclusive] - // google.protobuf.Duration other_value = 3 [(buf.validate.field).duration = { gte: { seconds: 10 }, lt: { seconds: 5 } }]; - // } - // ``` - google.protobuf.Duration gte = 6 [ - (predefined).cel = { - id: "duration.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "duration.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "duration.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "duration.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "duration.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - } - - // `in` asserts that the field must be equal to one of the specified values of the `google.protobuf.Duration` type. - // If the field's value doesn't correspond to any of the specified values, - // an error message will be generated. - // - // ```proto - // message MyDuration { - // // must be in list [1s, 2s, 3s] - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.in = ["1s", "2s", "3s"]]; - // } - // ``` - repeated google.protobuf.Duration in = 7 [(predefined).cel = { - id: "duration.in" - expression: "!(this in getField(rules, 'in')) ? 'must be in list %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` denotes that the field must not be equal to - // any of the specified values of the `google.protobuf.Duration` type. - // If the field's value matches any of these values, an error message will be - // generated. - // - // ```proto - // message MyDuration { - // // value must not be in list [1s, 2s, 3s] - // google.protobuf.Duration value = 1 [(buf.validate.field).duration.not_in = ["1s", "2s", "3s"]]; - // } - // ``` - repeated google.protobuf.Duration not_in = 8 [(predefined).cel = { - id: "duration.not_in" - expression: "this in rules.not_in ? 'must not be in list %s'.format([rules.not_in]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyDuration { - // google.protobuf.Duration value = 1 [ - // (buf.validate.field).duration.example = { seconds: 1 }, - // (buf.validate.field).duration.example = { seconds: 2 }, - // ]; - // } - // ``` - repeated google.protobuf.Duration example = 9 [(predefined).cel = { - id: "duration.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// FieldMaskRules describe rules applied exclusively to the `google.protobuf.FieldMask` well-known type. -message FieldMaskRules { - // `const` dictates that the field must match the specified value of the `google.protobuf.FieldMask` type exactly. - // If the field's value deviates from the specified value, an error message - // will be generated. - // - // ```proto - // message MyFieldMask { - // // value must equal ["a"] - // google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask.const = { - // paths: ["a"] - // }]; - // } - // ``` - optional google.protobuf.FieldMask const = 1 [(predefined).cel = { - id: "field_mask.const" - expression: "this.paths != getField(rules, 'const').paths ? 'must equal paths %s'.format([getField(rules, 'const').paths]) : ''" - }]; - - // `in` requires the field value to only contain paths matching specified - // values or their subpaths. - // If any of the field value's paths doesn't match the rule, - // an error message is generated. - // See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask - // - // ```proto - // message MyFieldMask { - // // The `value` FieldMask must only contain paths listed in `in`. - // google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = { - // in: ["a", "b", "c.a"] - // }]; - // } - // ``` - repeated string in = 2 [(predefined).cel = { - id: "field_mask.in" - expression: "!this.paths.all(p, p in getField(rules, 'in') || getField(rules, 'in').exists(f, p.startsWith(f+'.'))) ? 'must only contain paths in %s'.format([getField(rules, 'in')]) : ''" - }]; - - // `not_in` requires the field value to not contain paths matching specified - // values or their subpaths. - // If any of the field value's paths matches the rule, - // an error message is generated. - // See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask - // - // ```proto - // message MyFieldMask { - // // The `value` FieldMask shall not contain paths listed in `not_in`. - // google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = { - // not_in: ["forbidden", "immutable", "c.a"] - // }]; - // } - // ``` - repeated string not_in = 3 [(predefined).cel = { - id: "field_mask.not_in" - expression: "!this.paths.all(p, !(p in getField(rules, 'not_in') || getField(rules, 'not_in').exists(f, p.startsWith(f+'.')))) ? 'must not contain any paths in %s'.format([getField(rules, 'not_in')]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyFieldMask { - // google.protobuf.FieldMask value = 1 [ - // (buf.validate.field).field_mask.example = { paths: ["a", "b"] }, - // (buf.validate.field).field_mask.example = { paths: ["c.a", "d"] }, - // ]; - // } - // ``` - repeated google.protobuf.FieldMask example = 4 [(predefined).cel = { - id: "field_mask.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type. -message TimestampRules { - // `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated. - // - // ```proto - // message MyTimestamp { - // // value must equal 2023-05-03T10:00:00Z - // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.const = {seconds: 1727998800}]; - // } - // ``` - optional google.protobuf.Timestamp const = 2 [(predefined).cel = { - id: "timestamp.const" - expression: "this != getField(rules, 'const') ? 'must equal %s'.format([getField(rules, 'const')]) : ''" - }]; - oneof less_than { - // `lt` requires the timestamp field value to be less than the specified value (field < value). If the field value doesn't meet the required conditions, an error message is generated. - // - // ```proto - // message MyTimestamp { - // // timestamp must be less than '2023-01-01T00:00:00Z' [timestamp.lt] - // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lt = { seconds: 1672444800 }]; - // } - // ``` - google.protobuf.Timestamp lt = 3 [(predefined).cel = { - id: "timestamp.lt" - expression: - "!has(rules.gte) && !has(rules.gt) && this >= rules.lt" - "? 'must be less than %s'.format([rules.lt]) : ''" - }]; - - // `lte` requires the timestamp field value to be less than or equal to the specified value (field <= value). If the field value doesn't meet the required conditions, an error message is generated. - // - // ```proto - // message MyTimestamp { - // // timestamp must be less than or equal to '2023-05-14T00:00:00Z' [timestamp.lte] - // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.lte = { seconds: 1678867200 }]; - // } - // ``` - google.protobuf.Timestamp lte = 4 [(predefined).cel = { - id: "timestamp.lte" - expression: - "!has(rules.gte) && !has(rules.gt) && this > rules.lte" - "? 'must be less than or equal to %s'.format([rules.lte]) : ''" - }]; - - // `lt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be less than the current time. `lt_now` can only be used with the `within` rule. - // - // ```proto - // message MyTimestamp { - // // must be less than now - // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.lt_now = true]; - // } - // ``` - bool lt_now = 7 [(predefined).cel = { - id: "timestamp.lt_now" - expression: "(rules.lt_now && this > now) ? 'must be less than now' : ''" - }]; - } - oneof greater_than { - // `gt` requires the timestamp field value to be greater than the specified - // value (exclusive). If the value of `gt` is larger than a specified `lt` - // or `lte`, the range is reversed, and the field value must be outside the - // specified range. If the field value doesn't meet the required conditions, - // an error message is generated. - // - // ```proto - // message MyTimestamp { - // // timestamp must be greater than '2023-01-01T00:00:00Z' [timestamp.gt] - // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt = { seconds: 1672444800 }]; - // - // // timestamp must be greater than '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gt_lt] - // google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gt: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }]; - // - // // timestamp must be greater than '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gt_lt_exclusive] - // google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gt: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }]; - // } - // ``` - google.protobuf.Timestamp gt = 5 [ - (predefined).cel = { - id: "timestamp.gt" - expression: - "!has(rules.lt) && !has(rules.lte) && this <= rules.gt" - "? 'must be greater than %s'.format([rules.gt]) : ''" - }, - (predefined).cel = { - id: "timestamp.gt_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gt && (this >= rules.lt || this <= rules.gt)" - "? 'must be greater than %s and less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "timestamp.gt_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gt && (rules.lt <= this && this <= rules.gt)" - "? 'must be greater than %s or less than %s'.format([rules.gt, rules.lt]) : ''" - }, - (predefined).cel = { - id: "timestamp.gt_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gt && (this > rules.lte || this <= rules.gt)" - "? 'must be greater than %s and less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - }, - (predefined).cel = { - id: "timestamp.gt_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gt && (rules.lte < this && this <= rules.gt)" - "? 'must be greater than %s or less than or equal to %s'.format([rules.gt, rules.lte]) : ''" - } - ]; - - // `gte` requires the timestamp field value to be greater than or equal to the - // specified value (exclusive). If the value of `gte` is larger than a - // specified `lt` or `lte`, the range is reversed, and the field value - // must be outside the specified range. If the field value doesn't meet - // the required conditions, an error message is generated. - // - // ```proto - // message MyTimestamp { - // // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' [timestamp.gte] - // google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gte = { seconds: 1672444800 }]; - // - // // timestamp must be greater than or equal to '2023-01-01T00:00:00Z' and less than '2023-01-02T00:00:00Z' [timestamp.gte_lt] - // google.protobuf.Timestamp another_value = 2 [(buf.validate.field).timestamp = { gte: { seconds: 1672444800 }, lt: { seconds: 1672531200 } }]; - // - // // timestamp must be greater than or equal to '2023-01-02T00:00:00Z' or less than '2023-01-01T00:00:00Z' [timestamp.gte_lt_exclusive] - // google.protobuf.Timestamp other_value = 3 [(buf.validate.field).timestamp = { gte: { seconds: 1672531200 }, lt: { seconds: 1672444800 } }]; - // } - // ``` - google.protobuf.Timestamp gte = 6 [ - (predefined).cel = { - id: "timestamp.gte" - expression: - "!has(rules.lt) && !has(rules.lte) && this < rules.gte" - "? 'must be greater than or equal to %s'.format([rules.gte]) : ''" - }, - (predefined).cel = { - id: "timestamp.gte_lt" - expression: - "has(rules.lt) && rules.lt >= rules.gte && (this >= rules.lt || this < rules.gte)" - "? 'must be greater than or equal to %s and less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "timestamp.gte_lt_exclusive" - expression: - "has(rules.lt) && rules.lt < rules.gte && (rules.lt <= this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than %s'.format([rules.gte, rules.lt]) : ''" - }, - (predefined).cel = { - id: "timestamp.gte_lte" - expression: - "has(rules.lte) && rules.lte >= rules.gte && (this > rules.lte || this < rules.gte)" - "? 'must be greater than or equal to %s and less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - }, - (predefined).cel = { - id: "timestamp.gte_lte_exclusive" - expression: - "has(rules.lte) && rules.lte < rules.gte && (rules.lte < this && this < rules.gte)" - "? 'must be greater than or equal to %s or less than or equal to %s'.format([rules.gte, rules.lte]) : ''" - } - ]; - - // `gt_now` specifies that this field, of the `google.protobuf.Timestamp` type, must be greater than the current time. `gt_now` can only be used with the `within` rule. - // - // ```proto - // message MyTimestamp { - // // must be greater than now - // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.gt_now = true]; - // } - // ``` - bool gt_now = 8 [(predefined).cel = { - id: "timestamp.gt_now" - expression: "(rules.gt_now && this < now) ? 'must be greater than now' : ''" - }]; - } - - // `within` specifies that this field, of the `google.protobuf.Timestamp` type, must be within the specified duration of the current time. If the field value isn't within the duration, an error message is generated. - // - // ```proto - // message MyTimestamp { - // // must be within 1 hour of now - // google.protobuf.Timestamp created_at = 1 [(buf.validate.field).timestamp.within = {seconds: 3600}]; - // } - // ``` - optional google.protobuf.Duration within = 9 [(predefined).cel = { - id: "timestamp.within" - expression: "this < now-rules.within || this > now+rules.within ? 'must be within %s of now'.format([rules.within]) : ''" - }]; - - // `example` specifies values that the field may have. These values SHOULD - // conform to other rules. `example` values will not impact validation - // but may be used as helpful guidance on how to populate the given field. - // - // ```proto - // message MyTimestamp { - // google.protobuf.Timestamp value = 1 [ - // (buf.validate.field).timestamp.example = { seconds: 1672444800 }, - // (buf.validate.field).timestamp.example = { seconds: 1672531200 }, - // ]; - // } - // ``` - repeated google.protobuf.Timestamp example = 10 [(predefined).cel = { - id: "timestamp.example" - expression: "true" - }]; - - // Extension fields that have the (buf.validate.predefined) option set - // will be treated as predefined field rules. - // See https://protovalidate.com/schemas/predefined-rules/ - extensions 1000 to max; -} - -// `Violations` is a collection of `Violation` messages. This message type is returned by -// Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules. -// Each individual violation is represented by a `Violation` message. -message Violations { - // `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected. - repeated Violation violations = 1; -} - -// `Violation` represents a single instance where a validation rule, expressed -// as a `Rule`, was not met. It provides information about the field that -// caused the violation, the specific rule that wasn't fulfilled, and a -// human-readable error message. -// -// For example, consider the following message: -// -// ```proto -// message User { -// int32 age = 1 [(buf.validate.field).cel = { -// id: "user.age", -// expression: "this < 18 ? 'User must be at least 18 years old' : ''", -// }]; -// } -// ``` -// -// It could produce the following violation: -// -// ```json -// { -// "ruleId": "user.age", -// "message": "User must be at least 18 years old", -// "field": { -// "elements": [ -// { -// "fieldNumber": 1, -// "fieldName": "age", -// "fieldType": "TYPE_INT32" -// } -// ] -// }, -// "rule": { -// "elements": [ -// { -// "fieldNumber": 23, -// "fieldName": "cel", -// "fieldType": "TYPE_MESSAGE", -// "index": "0" -// } -// ] -// } -// } -// ``` -message Violation { - // `field` is a machine-readable path to the field that failed validation. - // This could be a nested field, in which case the path will include all the parent fields leading to the actual field that caused the violation. - // - // For example, consider the following message: - // - // ```proto - // message Message { - // bool a = 1 [(buf.validate.field).required = true]; - // } - // ``` - // - // It could produce the following violation: - // - // ```textproto - // violation { - // field { element { field_number: 1, field_name: "a", field_type: 8 } } - // ... - // } - // ``` - optional FieldPath field = 5; - - // `rule` is a machine-readable path that points to the specific rule that failed validation. - // This will be a nested field starting from the FieldRules of the field that failed validation. - // For custom rules, this will provide the path of the rule, e.g. `cel[0]`. - // - // For example, consider the following message: - // - // ```proto - // message Message { - // bool a = 1 [(buf.validate.field).required = true]; - // bool b = 2 [(buf.validate.field).cel = { - // id: "custom_rule", - // expression: "!this ? 'b must be true': ''" - // }] - // } - // ``` - // - // It could produce the following violations: - // - // ```textproto - // violation { - // rule { element { field_number: 25, field_name: "required", field_type: 8 } } - // ... - // } - // violation { - // rule { element { field_number: 23, field_name: "cel", field_type: 11, index: 0 } } - // ... - // } - // ``` - optional FieldPath rule = 6; - - // `rule_id` is the unique identifier of the `Rule` that was not fulfilled. - // This is the same `id` that was specified in the `Rule` message, allowing easy tracing of which rule was violated. - optional string rule_id = 2; - - // `message` is a human-readable error message that describes the nature of the violation. - // This can be the default error message from the violated `Rule`, or it can be a custom message that gives more context about the violation. - optional string message = 3; - - // `for_key` indicates whether the violation was caused by a map key, rather than a value. - optional bool for_key = 4; - - reserved 1; - reserved "field_path"; -} - -// `FieldPath` provides a path to a nested protobuf field. -// -// This message provides enough information to render a dotted field path even without protobuf descriptors. -// It also provides enough information to resolve a nested field through unknown wire data. -message FieldPath { - // `elements` contains each element of the path, starting from the root and recursing downward. - repeated FieldPathElement elements = 1; -} - -// `FieldPathElement` provides enough information to nest through a single protobuf field. -// -// If the selected field is a map or repeated field, the `subscript` value selects a specific element from it. -// A path that refers to a value nested under a map key or repeated field index will have a `subscript` value. -// The `field_type` field allows unambiguous resolution of a field even if descriptors are not available. -message FieldPathElement { - // `field_number` is the field number this path element refers to. - optional int32 field_number = 1; - - // `field_name` contains the field name this path element refers to. - // This can be used to display a human-readable path even if the field number is unknown. - optional string field_name = 2; - - // `field_type` specifies the type of this field. When using reflection, this value is not needed. - // - // This value is provided to make it possible to traverse unknown fields through wire data. - // When traversing wire data, be mindful of both packed[1] and delimited[2] encoding schemes. - // - // [1]: https://protobuf.dev/programming-guides/encoding/#packed - // [2]: https://protobuf.dev/programming-guides/encoding/#groups - // - // N.B.: Although groups are deprecated, the corresponding delimited encoding scheme is not, and - // can be explicitly used in Protocol Buffers 2023 Edition. - optional google.protobuf.FieldDescriptorProto.Type field_type = 3; - - // `key_type` specifies the map key type of this field. This value is useful when traversing - // unknown fields through wire data: specifically, it allows handling the differences between - // different integer encodings. - optional google.protobuf.FieldDescriptorProto.Type key_type = 4; - - // `value_type` specifies map value type of this field. This is useful if you want to display a - // value inside unknown fields through wire data. - optional google.protobuf.FieldDescriptorProto.Type value_type = 5; - - // `subscript` contains a repeated index or map key, if this path element nests into a repeated or map field. - oneof subscript { - // `index` specifies a 0-based index into a repeated field. - uint64 index = 6; - - // `bool_key` specifies a map key of type bool. - bool bool_key = 7; - - // `int_key` specifies a map key of type int32, int64, sint32, sint64, sfixed32 or sfixed64. - int64 int_key = 8; - - // `uint_key` specifies a map key of type uint32, uint64, fixed32 or fixed64. - uint64 uint_key = 9; - - // `string_key` specifies a map key of type string. - string string_key = 10; - } -} diff --git a/src/test/java/build/buf/protovalidate/BoolRulesEvaluatorTest.java b/src/test/java/build/buf/protovalidate/BoolRulesEvaluatorTest.java deleted file mode 100644 index ddb32fa7a..000000000 --- a/src/test/java/build/buf/protovalidate/BoolRulesEvaluatorTest.java +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.BoolRules; -import com.example.noimports.validationtest.ExampleBoolConst; -import com.google.protobuf.Descriptors.FieldDescriptor; -import org.junit.jupiter.api.Test; - -/** - * Validator-level integration tests for {@link BoolRulesEvaluator}. Mirrors the per-rule tests in - * protovalidate-go's {@code native_bool_test.go}, plus an explicit assertion on {@link - * Violation#getRuleValue()} since the conformance suite cannot detect divergence on that field (it - * is not part of the {@code Violation} proto schema — see CHANGELOG Phase 1). - */ -class BoolRulesEvaluatorTest { - - private static Validator nativeValidator() { - Config config = Config.newBuilder().setEnableNativeRules(true).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void boolConstPasses() throws ValidationException { - Validator validator = nativeValidator(); - ExampleBoolConst msg = ExampleBoolConst.newBuilder().setFlag(true).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.getViolations()).isEmpty(); - assertThat(result.isSuccess()).isTrue(); - } - - @Test - void boolConstFailsAndCarriesExpectedViolationShape() throws ValidationException { - Validator validator = nativeValidator(); - // Default value (false) fails the const=true rule. Bool fields without explicit-presence in - // proto3 still apply rules at default; bool has no IGNORE_IF_ZERO_VALUE behavior here. - ExampleBoolConst msg = ExampleBoolConst.newBuilder().setFlag(false).build(); - ValidationResult result = validator.validate(msg); - - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations()).hasSize(1); - - Violation violation = result.getViolations().get(0); - build.buf.validate.Violation proto = violation.toProto(); - assertThat(proto.getRuleId()).isEqualTo("bool.const"); - assertThat(proto.getMessage()).isEqualTo("must equal true"); - assertThat(proto.getField().getElementsList()).hasSize(1); - assertThat(proto.getField().getElements(0).getFieldName()).isEqualTo("flag"); - assertThat(proto.getRule().getElementsList()).hasSize(2); - assertThat(proto.getRule().getElements(0).getFieldName()).isEqualTo("bool"); - assertThat(proto.getRule().getElements(1).getFieldName()).isEqualTo("const"); - - // getRuleValue is NOT in the Violation proto — it's only on the Java wrapper. Conformance - // can't catch divergence on this field, so assert it explicitly. See Phase 1 CHANGELOG. - Violation.FieldValue ruleValue = violation.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo(true); - FieldDescriptor expectedRuleDesc = - BoolRules.getDescriptor().findFieldByNumber(BoolRules.CONST_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedRuleDesc); - - Violation.FieldValue fieldValue = violation.getFieldValue(); - assertThat(fieldValue).isNotNull(); - assertThat(fieldValue.getValue()).isEqualTo(false); - } - - @Test - void nativeAndCelProducePartiallyEqualViolations() throws ValidationException { - // Same input, both modes — toProto() must match exactly. (rule_value isn't in the proto so - // CEL/native can disagree there; that's covered by the dedicated assertion above.) - ExampleBoolConst msg = ExampleBoolConst.newBuilder().setFlag(false).build(); - - ValidationResult nativeResult = nativeValidator().validate(msg); - Validator celValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - ValidationResult celResult = celValidator.validate(msg); - - assertThat(nativeResult.getViolations()).hasSize(1); - assertThat(celResult.getViolations()).hasSize(1); - assertThat(nativeResult.getViolations().get(0).toProto()) - .isEqualTo(celResult.getViolations().get(0).toProto()); - } - - @Test - void nativeDispatchClearsRuleSoCelDoesNotDuplicateIt() throws ValidationException { - // If the dispatcher failed to clear bool.const on the residual FieldRules, CEL would also - // produce a violation and we'd see two. One violation proves clone-and-clear works. - ExampleBoolConst msg = ExampleBoolConst.newBuilder().setFlag(false).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - } -} diff --git a/src/test/java/build/buf/protovalidate/BytesRulesEvaluatorTest.java b/src/test/java/build/buf/protovalidate/BytesRulesEvaluatorTest.java deleted file mode 100644 index eb8efbb2d..000000000 --- a/src/test/java/build/buf/protovalidate/BytesRulesEvaluatorTest.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import build.buf.protovalidate.exceptions.ExecutionException; -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.BytesRules; -import com.example.noimports.validationtest.ExampleBytesConst; -import com.example.noimports.validationtest.ExampleBytesIPv4; -import com.example.noimports.validationtest.ExampleBytesPattern; -import com.google.protobuf.ByteString; -import com.google.protobuf.Descriptors.FieldDescriptor; -import org.junit.jupiter.api.Test; - -/** Validator-level tests for {@link BytesRulesEvaluator}. */ -class BytesRulesEvaluatorTest { - - private static Validator nativeValidator() { - Config config = Config.newBuilder().setEnableNativeRules(true).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void bytesConstFailsAndCarriesExpectedShape() throws ValidationException { - // const = "\x00\x99". Empty value (default) doesn't match. - ExampleBytesConst msg = ExampleBytesConst.newBuilder().setVal(ByteString.EMPTY).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - Violation v = result.getViolations().get(0); - - build.buf.validate.Violation proto = v.toProto(); - assertThat(proto.getRuleId()).isEqualTo("bytes.const"); - assertThat(proto.getMessage()).isEqualTo("must be 0099"); - assertThat(proto.getRule().getElements(0).getFieldName()).isEqualTo("bytes"); - assertThat(proto.getRule().getElements(1).getFieldName()).isEqualTo("const"); - - // rule_value isn't in the proto — assert directly. - Violation.FieldValue ruleValue = v.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo(ByteString.copyFrom(new byte[] {0x00, (byte) 0x99})); - FieldDescriptor expectedDesc = - BytesRules.getDescriptor().findFieldByNumber(BytesRules.CONST_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void bytesPatternMatchesAlphanumericOnly() throws ValidationException { - Validator v = nativeValidator(); - assertThat( - v.validate( - ExampleBytesPattern.newBuilder().setVal(ByteString.copyFromUtf8("abc")).build()) - .isSuccess()) - .isTrue(); - assertThat( - v.validate( - ExampleBytesPattern.newBuilder() - .setVal(ByteString.copyFromUtf8("abc1")) - .build()) - .isSuccess()) - .isFalse(); - } - - @Test - void bytesPatternThrowsOnInvalidUtf8() { - // Non-UTF-8 input + pattern rule → ExecutionException, matching Go's RuntimeError. - ByteString invalidUtf8 = ByteString.copyFrom(new byte[] {(byte) 0xFF, (byte) 0xFE}); - ExampleBytesPattern msg = ExampleBytesPattern.newBuilder().setVal(invalidUtf8).build(); - Validator v = nativeValidator(); - assertThatThrownBy(() -> v.validate(msg)) - .isInstanceOf(ExecutionException.class) - .hasMessageContaining("UTF-8"); - } - - @Test - void bytesIpv4WellKnownAcceptsFourBytes() throws ValidationException { - Validator v = nativeValidator(); - // 4 bytes — valid IPv4 size. - assertThat( - v.validate( - ExampleBytesIPv4.newBuilder().setVal(ByteString.copyFrom(new byte[4])).build()) - .isSuccess()) - .isTrue(); - } - - @Test - void bytesIpv4WellKnownRejectsWrongSize() throws ValidationException { - // 8 bytes — neither 0 nor 4, fails with the non-empty rule id. - ExampleBytesIPv4 msg = - ExampleBytesIPv4.newBuilder().setVal(ByteString.copyFrom(new byte[8])).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("bytes.ipv4"); - assertThat(proto.getMessage()).isEqualTo("must be a valid IPv4 address"); - } - - @Test - void nativeAndCelProduceEqualViolationProto() throws ValidationException { - ExampleBytesConst msg = ExampleBytesConst.newBuilder().setVal(ByteString.EMPTY).build(); - Validator nativeV = nativeValidator(); - Validator celV = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - assertThat(nativeV.validate(msg).getViolations().get(0).toProto()) - .isEqualTo(celV.validate(msg).getViolations().get(0).toProto()); - } -} diff --git a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java b/src/test/java/build/buf/protovalidate/CustomOverloadTest.java deleted file mode 100644 index 73658dc0e..000000000 --- a/src/test/java/build/buf/protovalidate/CustomOverloadTest.java +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import dev.cel.bundle.Cel; -import dev.cel.common.CelAbstractSyntaxTree; -import dev.cel.common.CelValidationException; -import dev.cel.common.CelValidationResult; -import dev.cel.runtime.CelEvaluationException; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; -import org.junit.jupiter.api.Test; - -public class CustomOverloadTest { - - private final Cel cel = ValidateLibrary.newCel(); - - @Test - public void testIsInf() throws Exception { - assertThat(evalToBool("0.0.isInf()")).isFalse(); - assertThat(evalToBool("(1.0/0.0).isInf()")).isTrue(); - assertThat(evalToBool("(1.0/0.0).isInf(0)")).isTrue(); - assertThat(evalToBool("(1.0/0.0).isInf(1)")).isTrue(); - assertThat(evalToBool("(1.0/0.0).isInf(-1)")).isFalse(); - assertThat(evalToBool("(-1.0/0.0).isInf()")).isTrue(); - assertThat(evalToBool("(-1.0/0.0).isInf(0)")).isTrue(); - assertThat(evalToBool("(-1.0/0.0).isInf(1)")).isFalse(); - assertThat(evalToBool("(-1.0/0.0).isInf(-1)")).isTrue(); - } - - @Test - public void testIsInfUnsupported() { - for (String testCase : Arrays.asList("'abc'.isInf()", "0.0.isInf('abc')")) { - assertThatThrownBy(() -> evalToBool(testCase)).isInstanceOf(CelValidationException.class); - } - } - - @Test - public void testIsNan() throws Exception { - assertThat(evalToBool("0.0.isNan()")).isFalse(); - assertThat(evalToBool("(0.0/0.0).isNan()")).isTrue(); - assertThat(evalToBool("(1.0/0.0).isNan()")).isFalse(); - } - - @Test - public void testIsNanUnsupported() { - for (String testCase : Collections.singletonList("'foo'.isNan()")) { - assertThatThrownBy(() -> evalToBool(testCase)).isInstanceOf(CelValidationException.class); - } - } - - @Test - public void testUnique() throws Exception { - assertThat(evalToBool("[].unique()")).isTrue(); - assertThat(evalToBool("[true].unique()")).isTrue(); - assertThat(evalToBool("[true, false].unique()")).isTrue(); - assertThat(evalToBool("[true, true].unique()")).isFalse(); - assertThat(evalToBool("[1, 2, 3].unique()")).isTrue(); - assertThat(evalToBool("[1, 2, 1].unique()")).isFalse(); - assertThat(evalToBool("[1u, 2u, 3u].unique()")).isTrue(); - assertThat(evalToBool("[1u, 2u, 2u].unique()")).isFalse(); - assertThat(evalToBool("[1.0, 2.0, 3.0].unique()")).isTrue(); - assertThat(evalToBool("[3.0,2.0,3.0].unique()")).isFalse(); - assertThat(evalToBool("['abc', 'def'].unique()")).isTrue(); - assertThat(evalToBool("['abc', 'abc'].unique()")).isFalse(); - assertThat(evalToBool("[b'abc', b'123'].unique()")).isTrue(); - assertThat(evalToBool("[b'123', b'123'].unique()")).isFalse(); - // Previously, the unique() method returned false here as both bytes were converted - // to UTF-8. Since both contain invalid UTF-8, this would lead to them treated as equal - // because they'd have the same substitution character. - assertThat(evalToBool("[b'\\xFF', b'\\xFE'].unique()")).isTrue(); - } - - @Test - public void testUniqueUnsupported() { - for (String testCase : Collections.singletonList("1.unique()")) { - assertThatThrownBy(() -> evalToBool(testCase)).isInstanceOf(CelValidationException.class); - } - } - - @Test - public void testIsIpPrefix() throws Exception { - assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix()")).isTrue(); - assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix()")).isTrue(); - assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(true)")).isTrue(); - assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix(true)")).isFalse(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix()")).isTrue(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix()")).isTrue(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(true)")) - .isTrue(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(true)")) - .isFalse(); - assertThat(evalToBool("'1.2.3.4'.isIpPrefix()")).isFalse(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b'.isIpPrefix()")).isFalse(); - assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(4)")).isTrue(); - assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix(4)")).isTrue(); - assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(4,true)")).isTrue(); - assertThat(evalToBool("'1.2.3.4/24'.isIpPrefix(4,true)")).isFalse(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(4)")).isFalse(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6)")).isTrue(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6)")).isTrue(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6,true)")) - .isTrue(); - assertThat(evalToBool("'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6,true)")) - .isFalse(); - assertThat(evalToBool("'1.2.3.0/24'.isIpPrefix(6)")).isFalse(); - } - - @Test - public void testIsIpPrefixUnsupported() { - for (String testCase : - Arrays.asList( - "1.isIpPrefix()", - "'1.2.3.0/24'.isIpPrefix('foo')", - "'1.2.3.0/24'.isIpPrefix(4,'foo')", - "'1.2.3.0/24'.isIpPrefix('foo',true)")) { - assertThatThrownBy(() -> eval(testCase)).isInstanceOf(CelValidationException.class); - } - } - - @Test - public void testIsHostname() throws Exception { - assertThat(evalToBool("'example.com'.isHostname()")).isTrue(); - assertThat(evalToBool("'example.123'.isHostname()")).isFalse(); - } - - @Test - public void testIsEmail() throws Exception { - assertThat(evalToBool("'foo@example.com'.isEmail()")).isTrue(); - assertThat(evalToBool("''.isEmail()")).isFalse(); - assertThat(evalToBool("' foo@example.com'.isEmail()")).isFalse(); - assertThat(evalToBool("'foo@example.com '.isEmail()")).isFalse(); - } - - @Test - public void testBytesContains() throws Exception { - assertThat(evalToBool("bytes('12345').contains(bytes(''))")).isTrue(); - assertThat(evalToBool("bytes('12345').contains(bytes('1'))")).isTrue(); - assertThat(evalToBool("bytes('12345').contains(bytes('5'))")).isTrue(); - assertThat(evalToBool("bytes('12345').contains(bytes('123'))")).isTrue(); - assertThat(evalToBool("bytes('12345').contains(bytes('234'))")).isTrue(); - assertThat(evalToBool("bytes('12345').contains(bytes('345'))")).isTrue(); - assertThat(evalToBool("bytes('12345').contains(bytes('12345'))")).isTrue(); - - assertThat(evalToBool("bytes('12345').contains(bytes('6'))")).isFalse(); - assertThat(evalToBool("bytes('12345').contains(bytes('13'))")).isFalse(); - assertThat(evalToBool("bytes('12345').contains(bytes('35'))")).isFalse(); - assertThat(evalToBool("bytes('12345').contains(bytes('123456'))")).isFalse(); - } - - @Test - public void testMatchesPartialMatch() throws Exception { - // CelOptions.DEFAULT sets enableRegexPartialMatch(true), so an unanchored regex should - // match anywhere in the input (find()), not require a full-string match. - assertThat(evalToBool("'hello world'.matches('world')")).isTrue(); - assertThat(evalToBool("'hello world'.matches('ell')")).isTrue(); - // Anchored patterns still behave the same. - assertThat(evalToBool("'hello'.matches('^hello$')")).isTrue(); - assertThat(evalToBool("'hello world'.matches('^hello$')")).isFalse(); - // Global form. - assertThat(evalToBool("matches('hello world', 'world')")).isTrue(); - } - - private Object eval(String source) throws Exception { - return eval(source, Collections.emptyMap()); - } - - private Object eval(String source, Map vars) - throws CelEvaluationException, CelValidationException { - CelValidationResult parsed = cel.compile(source); - CelAbstractSyntaxTree ast = parsed.getAst(); - return cel.createProgram(ast).eval(vars); - } - - private boolean evalToBool(String source) throws Exception { - return (Boolean) eval(source); - } -} diff --git a/src/test/java/build/buf/protovalidate/EnumRulesEvaluatorTest.java b/src/test/java/build/buf/protovalidate/EnumRulesEvaluatorTest.java deleted file mode 100644 index c3aad8e9b..000000000 --- a/src/test/java/build/buf/protovalidate/EnumRulesEvaluatorTest.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.EnumRules; -import com.example.noimports.validationtest.ExampleColor; -import com.example.noimports.validationtest.ExampleEnumConst; -import com.example.noimports.validationtest.ExampleEnumIn; -import com.google.protobuf.Descriptors.FieldDescriptor; -import org.junit.jupiter.api.Test; - -/** Validator-level tests for {@link EnumRulesEvaluator}. */ -class EnumRulesEvaluatorTest { - - private static Validator nativeValidator() { - Config config = Config.newBuilder().setEnableNativeRules(true).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void enumConstFailsAndCarriesExpectedShape() throws ValidationException { - // Default UNSPECIFIED (0) != const 2 (GREEN). - ExampleEnumConst msg = ExampleEnumConst.newBuilder().build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - Violation v = result.getViolations().get(0); - - build.buf.validate.Violation proto = v.toProto(); - assertThat(proto.getRuleId()).isEqualTo("enum.const"); - assertThat(proto.getMessage()).isEqualTo("must equal 2"); - assertThat(proto.getRule().getElements(0).getFieldName()).isEqualTo("enum"); - assertThat(proto.getRule().getElements(1).getFieldName()).isEqualTo("const"); - - // rule_value isn't in the Violation proto — assert it directly here. See Phase 1 CHANGELOG. - Violation.FieldValue ruleValue = v.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo(2); - FieldDescriptor expectedDesc = - EnumRules.getDescriptor().findFieldByNumber(EnumRules.CONST_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void enumInFailsForValueNotInList() throws ValidationException { - // Default UNSPECIFIED (0) not in [1, 3]. - ExampleEnumIn msg = ExampleEnumIn.newBuilder().build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("enum.in"); - assertThat(proto.getMessage()).isEqualTo("must be in list [1, 3]"); - } - - @Test - void enumInPassesForValueInList() throws ValidationException { - ExampleEnumIn msg = ExampleEnumIn.newBuilder().setVal(ExampleColor.EXAMPLE_COLOR_RED).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.isSuccess()).isTrue(); - } - - @Test - void nativeAndCelProduceEqualViolationProto() throws ValidationException { - ExampleEnumConst msg = ExampleEnumConst.newBuilder().build(); - Validator nativeV = nativeValidator(); - Validator celV = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - assertThat(nativeV.validate(msg).getViolations().get(0).toProto()) - .isEqualTo(celV.validate(msg).getViolations().get(0).toProto()); - } -} diff --git a/src/test/java/build/buf/protovalidate/FailFastTest.java b/src/test/java/build/buf/protovalidate/FailFastTest.java deleted file mode 100644 index 292ec6998..000000000 --- a/src/test/java/build/buf/protovalidate/FailFastTest.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.noimports.validationtest.BytesMultiRule; -import com.example.noimports.validationtest.Int32MultiRule; -import com.example.noimports.validationtest.StringMultiRule; -import com.google.protobuf.ByteString; -import com.google.protobuf.Message; -import org.junit.jupiter.api.Test; - -/** - * failFast tests for the native rule evaluators. Each fixture is constructed so the input violates - * two rules. Without failFast, both violations are reported; with failFast=true, the validator must - * short-circuit after the first. - */ -class FailFastTest { - - private static Validator validator(boolean failFast) { - Config config = Config.newBuilder().setEnableNativeRules(true).setFailFast(failFast).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void stringEvaluator_failFastSkipsLaterRules() throws ValidationException { - // "ab" — fails min_len=4, would also fail pattern .*[0-9].* - StringMultiRule msg = StringMultiRule.newBuilder().setVal("ab").build(); - assertTwoViolationsWithoutFailFastOneWith(msg); - } - - @Test - void numericEvaluator_failFastSkipsLaterRules() throws ValidationException { - // val=0: fails const=5 and fails gt=10 - Int32MultiRule msg = Int32MultiRule.newBuilder().setVal(0).build(); - assertTwoViolationsWithoutFailFastOneWith(msg); - } - - @Test - void bytesEvaluator_failFastSkipsLaterRules() throws ValidationException { - // 1-byte value — fails min_len=4 AND fails ipv4 size requirement. - BytesMultiRule msg = - BytesMultiRule.newBuilder().setVal(ByteString.copyFrom(new byte[] {0x01})).build(); - assertTwoViolationsWithoutFailFastOneWith(msg); - } - - private void assertTwoViolationsWithoutFailFastOneWith(Message msg) throws ValidationException { - ValidationResult full = validator(false).validate(msg); - ValidationResult fast = validator(true).validate(msg); - assertThat(full.getViolations()) - .as("without failFast, both violations should be reported") - .hasSize(2); - assertThat(fast.getViolations()) - .as("with failFast, only the first violation should be reported") - .hasSize(1); - } -} diff --git a/src/test/java/build/buf/protovalidate/FloatBugConfirmationTest.java b/src/test/java/build/buf/protovalidate/FloatBugConfirmationTest.java deleted file mode 100644 index dfc82d52a..000000000 --- a/src/test/java/build/buf/protovalidate/FloatBugConfirmationTest.java +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.imports.validationtest.FloatDoubleNaNNegZero; -import com.example.noimports.validationtest.ExampleDoubleConstNegZero; -import com.example.noimports.validationtest.ExampleDoubleRepeatedUnique; -import com.example.noimports.validationtest.ExampleFloatConstNegZero; -import com.example.noimports.validationtest.ExampleFloatRepeatedUnique; -import com.google.protobuf.Message; -import java.util.List; -import java.util.stream.Collectors; -import org.junit.jupiter.api.Test; - -/** - * Comparative tests for the floating-point findings in NATIVE_RULES_REVIEW.md (B1, B2). Each test - * runs the same input through the native and CEL evaluation paths and asserts on the resulting - * {@code Violation} protos. - * - *

Outcomes after running: - * - *

    - *
  • B1 — fixed. {@code floatFormatter}/{@code doubleFormatter} now check the sign bit on - * entry and return {@code "-0"} for negative zero, so {@code float.const = -0.0} produces the - * same violation message in both modes. The tests below lock in that parity. - *
  • B2 — reclassified. Original review claimed native diverges from CEL on {@code - * repeated.unique} for {@code NaN}/{@code -0.0}. Investigation showed CEL's {@code unique()} - * is registered by protovalidate-java itself (see {@code CustomOverload.uniqueList}) and uses - * {@code Object.equals} on a {@link java.util.HashSet} — the same defect as {@code - * RepeatedRulesEvaluator.isUnique}. So both paths agree (both deviate from the CEL spec, - * which mandates IEEE-754 equality on doubles). The tests below lock in that agreement so any - * future fix has to touch both paths together. - *
- */ -class FloatBugConfirmationTest { - - private final Validator nativeValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(true).build()) - .build(); - private final Validator celValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - - // --- B1: floatFormatter renders -0.0 as "0", losing the sign -------------------------------- - - @Test - void floatConstNegZero_messageMatchesBetweenNativeAndCel() throws ValidationException { - // After the floatFormatter fix, both modes report a violation (1.0 != -0.0) with the same - // message — the rule value is rendered as "-0" in both paths. This test locks in parity; - // a regression in floatFormatter (e.g. the sign-bit short-circuit being removed) will fail - // here. - ExampleFloatConstNegZero msg = ExampleFloatConstNegZero.newBuilder().setVal(1.0f).build(); - String nativeMsg = singleViolationMessage(nativeValidator, msg); - String celMsg = singleViolationMessage(celValidator, msg); - - assertThat(nativeMsg).isEqualTo("must equal -0"); - assertThat(celMsg).isEqualTo("must equal -0"); - } - - @Test - void doubleConstNegZero_messageMatchesBetweenNativeAndCel() throws ValidationException { - ExampleDoubleConstNegZero msg = ExampleDoubleConstNegZero.newBuilder().setVal(1.0).build(); - String nativeMsg = singleViolationMessage(nativeValidator, msg); - String celMsg = singleViolationMessage(celValidator, msg); - - assertThat(nativeMsg).isEqualTo("must equal -0"); - assertThat(celMsg).isEqualTo("must equal -0"); - } - - // --- B2: repeated.unique on floats — native and CEL agree (both wrong vs spec) -------------- - // - // CustomOverload.uniqueList (the CEL-side `unique()` registered by protovalidate-java) is - // implemented with HashSet + Object.equals — the same approach as RepeatedRulesEvaluator's - // native path. Both treat NaN as duplicate (Java equality) and +0.0/-0.0 as distinct - // (floatToIntBits), contradicting CEL's spec (IEEE-754: NaN != NaN, +0.0 == -0.0). - // - // These tests assert the agreement so any IEEE-754 fix has to ship in CustomOverload.uniqueList - // and RepeatedRulesEvaluator.isUnique together — otherwise NativeRulesParityTest will start - // failing. - - @Test - void floatRepeatedUnique_NaNNaN_bothPathsAgreeBothWrongVsSpec() throws ValidationException { - ExampleFloatRepeatedUnique msg = - ExampleFloatRepeatedUnique.newBuilder().addVal(Float.NaN).addVal(Float.NaN).build(); - assertViolationsEqual(msg); - } - - @Test - void doubleRepeatedUnique_NaNNaN_bothPathsAgreeBothWrongVsSpec() throws ValidationException { - ExampleDoubleRepeatedUnique msg = - ExampleDoubleRepeatedUnique.newBuilder().addVal(Double.NaN).addVal(Double.NaN).build(); - assertViolationsEqual(msg); - } - - @Test - void floatRepeatedUnique_PlusZeroMinusZero_bothPathsAgreeBothWrongVsSpec() - throws ValidationException { - ExampleFloatRepeatedUnique msg = - ExampleFloatRepeatedUnique.newBuilder().addVal(0.0f).addVal(-0.0f).build(); - assertViolationsEqual(msg); - } - - @Test - void doubleRepeatedUnique_PlusZeroMinusZero_bothPathsAgreeBothWrongVsSpec() - throws ValidationException { - ExampleDoubleRepeatedUnique msg = - ExampleDoubleRepeatedUnique.newBuilder().addVal(0.0).addVal(-0.0).build(); - assertViolationsEqual(msg); - } - - @Test - void floatDoubleNaNNegZero() throws ValidationException { - // these tests are also checking that an unset (zero) field is equal to -0 - FloatDoubleNaNNegZero nanMsg = - FloatDoubleNaNNegZero.newBuilder() - .addDvals(Double.NaN) - .addDvals(Double.NaN) - .addFvals(Float.NaN) - .addFvals(Float.NaN) - .build(); - // should both be no error, since NaN is not equal to itself - ValidationResult nanMsgResultNative = nativeValidator.validate(nanMsg); - ValidationResult nanMsgResultCEL = celValidator.validate(nanMsg); - assertViolationsEqual(nanMsg); - // works for native and CEL now - assertThat(nanMsgResultNative.getViolations()).isEmpty(); - assertThat(nanMsgResultCEL.getViolations()).isEmpty(); - - // now check -0 and 0 for uniqueness (should not be) - FloatDoubleNaNNegZero zeroMsg = - FloatDoubleNaNNegZero.newBuilder() - .addDvals(0.0) - .addDvals(-0.0) - .addFvals(0.0F) - .addFvals(-0.0F) - .build(); - // should both be error, since 0 == -0 - nanMsgResultNative = nativeValidator.validate(zeroMsg); - nanMsgResultCEL = celValidator.validate(zeroMsg); - assertViolationsEqual(zeroMsg); - assertThat(nanMsgResultNative.getViolations()).isNotEmpty(); - assertThat(nanMsgResultCEL.getViolations()).isNotEmpty(); - } - - // --- helpers ---------------------------------------------------------------------------------- - - private static String singleViolationMessage(Validator v, Message msg) - throws ValidationException { - ValidationResult result = v.validate(msg); - List messages = - result.getViolations().stream() - .map(violation -> violation.toProto().getMessage()) - .collect(Collectors.toList()); - assertThat(messages).hasSize(1); - return messages.get(0); - } - - private void assertViolationsEqual(Message msg) throws ValidationException { - List nativeProtos = toProtoList(nativeValidator.validate(msg)); - List celProtos = toProtoList(celValidator.validate(msg)); - assertThat(nativeProtos) - .as("native and CEL must produce identical Violation protos for %s", msg) - .isEqualTo(celProtos); - } - - private static List toProtoList(ValidationResult result) { - return result.getViolations().stream().map(v -> v.toProto()).collect(Collectors.toList()); - } -} diff --git a/src/test/java/build/buf/protovalidate/FormatTest.java b/src/test/java/build/buf/protovalidate/FormatTest.java deleted file mode 100644 index c167e03ae..000000000 --- a/src/test/java/build/buf/protovalidate/FormatTest.java +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.*; - -import cel.expr.conformance.proto3.TestAllTypes; -import com.cel.expr.Decl; -import com.cel.expr.ExprValue; -import com.cel.expr.Value; -import com.cel.expr.conformance.test.SimpleTest; -import com.cel.expr.conformance.test.SimpleTestFile; -import com.cel.expr.conformance.test.SimpleTestSection; -import com.google.protobuf.TextFormat; -import dev.cel.bundle.Cel; -import dev.cel.bundle.CelBuilder; -import dev.cel.common.CelValidationException; -import dev.cel.common.CelValidationResult; -import dev.cel.common.types.SimpleType; -import dev.cel.runtime.CelEvaluationException; -import dev.cel.runtime.CelRuntime.Program; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Named; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class FormatTest { - // Version of the cel-spec that this implementation is conformant with - // This should be kept in sync with the version in gradle.properties - private static final String CEL_SPEC_VERSION = "v0.24.0"; - - private static Cel cel; - - private static List formatTests; - private static List formatErrorTests; - - @BeforeAll - public static void setUp() throws Exception { - // The test data from the cel-spec conformance tests - List celSpecSections = - loadTestData("src/test/resources/testdata/string_ext_" + CEL_SPEC_VERSION + ".textproto"); - // Our supplemental tests of functionality not in the cel conformance file, but defined in the - // spec. - List supplementalSections = - loadTestData("src/test/resources/testdata/string_ext_supplemental.textproto"); - - // Combine the test data from both files into one - List sections = - Stream.concat(celSpecSections.stream(), supplementalSections.stream()) - .collect(Collectors.toList()); - - // Find the format tests which test successful formatting - formatTests = - sections.stream() - .filter(s -> s.getName().equals("format")) - .flatMap(s -> s.getTestList().stream()) - .collect(Collectors.toList()); - - // Find the format error tests which test errors during formatting - formatErrorTests = - sections.stream() - .filter(s -> s.getName().equals("format_errors")) - .flatMap(s -> s.getTestList().stream()) - .collect(Collectors.toList()); - - cel = ValidateLibrary.newCel(); - } - - @ParameterizedTest - @MethodSource("getFormatTests") - void testFormatSuccess(SimpleTest test) throws CelValidationException, CelEvaluationException { - Object result = evaluate(test); - assertThat(result).isEqualTo(getExpectedResult(test)); - assertThat(result).isInstanceOf(String.class); - } - - @ParameterizedTest - @MethodSource("getFormatErrorTests") - void testFormatError(SimpleTest test) { - assertThatThrownBy(() -> evaluate(test)).isInstanceOf(CelEvaluationException.class); - } - - // Loads test data from the given text format file - private static List loadTestData(String fileName) throws Exception { - byte[] encoded = Files.readAllBytes(Paths.get(fileName)); - String data = new String(encoded, StandardCharsets.UTF_8); - SimpleTestFile.Builder bldr = SimpleTestFile.newBuilder(); - TextFormat.getParser().merge(data, bldr); - SimpleTestFile testData = bldr.build(); - - return testData.getSectionList(); - } - - // Runs a test by extending the cel environment with the specified - // types, variables and declarations, then evaluating it with the cel runtime. - private static Object evaluate(SimpleTest test) - throws CelValidationException, CelEvaluationException { - - CelBuilder builder = cel.toCelBuilder().addMessageTypes(TestAllTypes.getDescriptor()); - addDecls(builder, test); - Cel newCel = builder.build(); - - CelValidationResult validationResult = newCel.compile(test.getExpr()); - if (!validationResult.getAllIssues().isEmpty()) { - fail("error building AST for evaluation: " + validationResult.getIssueString()); - } - Program program = newCel.createProgram(validationResult.getAst()); - return program.eval(buildVariables(test.getBindingsMap())); - } - - private static Stream getTestStream(List tests) { - List args = new ArrayList<>(); - for (SimpleTest test : tests) { - args.add(Arguments.arguments(Named.named(test.getName(), test))); - } - - return args.stream(); - } - - private static Stream getFormatTests() { - return getTestStream(formatTests); - } - - private static Stream getFormatErrorTests() { - return getTestStream(formatErrorTests); - } - - // Builds the variable definitions to be used during evaluation - private static Map buildVariables(Map bindings) { - Map vars = new HashMap<>(); - for (Map.Entry entry : bindings.entrySet()) { - ExprValue exprValue = entry.getValue(); - if (exprValue.hasValue()) { - Value val = exprValue.getValue(); - if (val.hasStringValue()) { - vars.put(entry.getKey(), val.getStringValue()); - } - } - } - return vars; - } - - // Gets the expected result for a given test - private static String getExpectedResult(SimpleTest test) { - if (test.hasValue()) { - if (test.getValue().hasStringValue()) { - return test.getValue().getStringValue(); - } - } else if (test.hasEvalError()) { - // Note that we only expect a single eval error for all the conformance tests - if (test.getEvalError().getErrorsList().size() == 1) { - return test.getEvalError().getErrorsList().get(0).getMessage(); - } - } - return ""; - } - - // Builds the declarations for a given test - private static void addDecls(CelBuilder builder, SimpleTest test) { - for (Decl decl : test.getTypeEnvList()) { - if (decl.hasIdent()) { - Decl.IdentDecl ident = decl.getIdent(); - com.cel.expr.Type type = ident.getType(); - if (type.hasPrimitive()) { - if (type.getPrimitive() == com.cel.expr.Type.PrimitiveType.STRING) { - builder.addVar(decl.getName(), SimpleType.STRING); - } - } - } - } - } -} diff --git a/src/test/java/build/buf/protovalidate/Issue427Test.java b/src/test/java/build/buf/protovalidate/Issue427Test.java deleted file mode 100644 index b112a63aa..000000000 --- a/src/test/java/build/buf/protovalidate/Issue427Test.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.validate.Violation; -import org.junit.jupiter.api.Test; - -public class Issue427Test { - @Test - public void testMessageOneofWithNameOnly() throws Exception { - com.example.imports.validationtest.Issue427 msg = - com.example.imports.validationtest.Issue427.newBuilder().setName("foo").build(); - Validator validator = ValidatorFactory.newBuilder().build(); - assertThat(validator.validate(msg).toProto().getViolationsList()).isEmpty(); - } - - @Test - public void testMessageOneofWithTagsOnly() throws Exception { - com.example.imports.validationtest.Issue427 msg = - com.example.imports.validationtest.Issue427.newBuilder().addTags("a").addTags("b").build(); - Validator validator = ValidatorFactory.newBuilder().build(); - assertThat(validator.validate(msg).toProto().getViolationsList()).isEmpty(); - } - - @Test - public void testMessageOneofWithMappingsOnly() throws Exception { - com.example.imports.validationtest.Issue427 msg = - com.example.imports.validationtest.Issue427.newBuilder().putMappings("k", "v").build(); - Validator validator = ValidatorFactory.newBuilder().build(); - assertThat(validator.validate(msg).toProto().getViolationsList()).isEmpty(); - } - - @Test - public void testMessageOneofNoneSet() throws Exception { - com.example.imports.validationtest.Issue427 msg = - com.example.imports.validationtest.Issue427.getDefaultInstance(); - Validator validator = ValidatorFactory.newBuilder().build(); - assertThat(validator.validate(msg).toProto().getViolationsList()) - .containsExactly( - Violation.newBuilder() - .setRuleId("message.oneof") - .setMessage("one of name, tags, mappings must be set") - .build()); - } -} diff --git a/src/test/java/build/buf/protovalidate/NotInRulesTest.java b/src/test/java/build/buf/protovalidate/NotInRulesTest.java deleted file mode 100644 index d12219521..000000000 --- a/src/test/java/build/buf/protovalidate/NotInRulesTest.java +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.noimports.validationtest.BytesNotIn; -import com.example.noimports.validationtest.EnumNotIn; -import com.example.noimports.validationtest.ExampleColor; -import com.example.noimports.validationtest.Int32NotIn; -import com.example.noimports.validationtest.StringNotIn; -import com.example.noimports.validationtest.Uint32NotIn; -import com.google.protobuf.ByteString; -import org.junit.jupiter.api.Test; - -/** - * Targeted not_in coverage for each native evaluator. The conformance suite covers behavior; what - * these tests pin down is the rule_id and message text the native path produces, since those are - * the contract observable from {@code Violation.toProto()}. - */ -class NotInRulesTest { - - private final Validator validator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(true).build()) - .build(); - - @Test - void int32NotIn() throws ValidationException { - Int32NotIn msg = Int32NotIn.newBuilder().setVal(2).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - assertThat(result.getViolations().get(0).toProto().getRuleId()).isEqualTo("int32.not_in"); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("must not be in list [1, 2, 3]"); - } - - @Test - void uint32NotIn() throws ValidationException { - Uint32NotIn msg = Uint32NotIn.newBuilder().setVal(1).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - assertThat(result.getViolations().get(0).toProto().getRuleId()).isEqualTo("uint32.not_in"); - } - - @Test - void stringNotIn() throws ValidationException { - StringNotIn msg = StringNotIn.newBuilder().setVal("foo").build(); - ValidationResult result = validator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - assertThat(result.getViolations().get(0).toProto().getRuleId()).isEqualTo("string.not_in"); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("must not be in list [foo, bar]"); - } - - @Test - void bytesNotIn() throws ValidationException { - BytesNotIn msg = BytesNotIn.newBuilder().setVal(ByteString.copyFromUtf8("AA")).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - assertThat(result.getViolations().get(0).toProto().getRuleId()).isEqualTo("bytes.not_in"); - } - - @Test - void enumNotIn() throws ValidationException { - EnumNotIn msg = EnumNotIn.newBuilder().setVal(ExampleColor.EXAMPLE_COLOR_RED).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - assertThat(result.getViolations().get(0).toProto().getRuleId()).isEqualTo("enum.not_in"); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("must not be in list [1, 2]"); - } -} diff --git a/src/test/java/build/buf/protovalidate/NumericRulesEvaluatorTest.java b/src/test/java/build/buf/protovalidate/NumericRulesEvaluatorTest.java deleted file mode 100644 index 7c97ada89..000000000 --- a/src/test/java/build/buf/protovalidate/NumericRulesEvaluatorTest.java +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.DoubleRules; -import build.buf.validate.FloatRules; -import build.buf.validate.Int32Rules; -import build.buf.validate.UInt32Rules; -import com.example.noimports.validationtest.ExampleDoubleIn; -import com.example.noimports.validationtest.ExampleFloatFinite; -import com.example.noimports.validationtest.ExampleInt32Const; -import com.example.noimports.validationtest.ExampleInt32GtLt; -import com.example.noimports.validationtest.ExampleUint32Gt; -import com.google.protobuf.Descriptors.FieldDescriptor; -import java.util.Arrays; -import org.junit.jupiter.api.Test; - -/** - * Validator-level tests for {@link NumericRulesEvaluator}. Per-kind comparison correctness is - * already covered comprehensively by the conformance suite (44 cases × 12 kinds); these tests focus - * on the things conformance can't catch: - * - *
    - *
  • {@link Violation#getRuleValue()} shape — not part of the {@code Violation} proto so the - * conformance harness can't assert on it. - *
  • Unsigned comparison correctness with values above {@code Integer.MAX_VALUE} as a targeted - * regression test for {@code Integer.compareUnsigned} wiring. - *
  • {@code finite}-rule {@code NaN}/{@code Inf} dispatch. - *
- */ -class NumericRulesEvaluatorTest { - - private static Validator nativeValidator() { - Config config = Config.newBuilder().setEnableNativeRules(true).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void int32ConstFailsAndCarriesExpectedShape() throws ValidationException { - // Default 0 != const 5. - ExampleInt32Const msg = ExampleInt32Const.newBuilder().setVal(0).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - - Violation v = result.getViolations().get(0); - build.buf.validate.Violation proto = v.toProto(); - assertThat(proto.getRuleId()).isEqualTo("int32.const"); - assertThat(proto.getMessage()).isEqualTo("must equal 5"); - assertThat(proto.getField().getElements(0).getFieldName()).isEqualTo("val"); - assertThat(proto.getRule().getElements(0).getFieldName()).isEqualTo("int32"); - assertThat(proto.getRule().getElements(1).getFieldName()).isEqualTo("const"); - - // Action item from Phase 1 / Task #3: rule_value isn't in the Violation proto, so the - // conformance suite can't catch divergence on it. Assert directly here. - Violation.FieldValue ruleValue = v.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo(5); - FieldDescriptor expectedDesc = - Int32Rules.getDescriptor().findFieldByNumber(Int32Rules.CONST_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void int32GtLtRangeProducesCombinedRuleId() throws ValidationException { - // Default 0 violates gt=0 (lower bound). Combined gt+lt produces "int32.gt_lt" rule id with - // a single combined message. - ExampleInt32GtLt msg = ExampleInt32GtLt.newBuilder().setVal(0).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("int32.gt_lt"); - assertThat(proto.getMessage()).isEqualTo("must be greater than 0 and less than 10"); - } - - @Test - void uint32UnsignedComparisonHandlesValuesAboveSignedMax() throws ValidationException { - // Rule: gt = 2147483648 (which is Integer.MAX_VALUE + 1 as unsigned). A naive signed compare - // would interpret this threshold as -2147483648 and accept any positive int. With unsigned - // semantics, 1 must NOT satisfy gt=2147483648. - ExampleUint32Gt msg = ExampleUint32Gt.newBuilder().setVal(1).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("uint32.gt"); - assertThat(proto.getMessage()).isEqualTo("must be greater than 2147483648"); - - Violation.FieldValue ruleValue = result.getViolations().get(0).getRuleValue(); - assertThat(ruleValue).isNotNull(); - // Stored as Java's signed Integer with the bit pattern of unsigned 2147483648. - assertThat(ruleValue.getValue()).isEqualTo(Integer.MIN_VALUE); - FieldDescriptor expectedDesc = - UInt32Rules.getDescriptor().findFieldByNumber(UInt32Rules.GT_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void uint32UnsignedComparisonAcceptsValueAboveThreshold() throws ValidationException { - // 3000000000 (unsigned) must satisfy gt=2147483648 (unsigned). - ExampleUint32Gt msg = ExampleUint32Gt.newBuilder().setVal((int) 3_000_000_000L).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.isSuccess()).isTrue(); - assertThat(result.getViolations()).isEmpty(); - } - - @Test - void floatFiniteFailsForNaNAndInf() throws ValidationException { - Validator v = nativeValidator(); - assertThat(v.validate(ExampleFloatFinite.newBuilder().setVal(Float.NaN).build()).isSuccess()) - .isFalse(); - assertThat( - v.validate(ExampleFloatFinite.newBuilder().setVal(Float.POSITIVE_INFINITY).build()) - .isSuccess()) - .isFalse(); - assertThat(v.validate(ExampleFloatFinite.newBuilder().setVal(1.0f).build()).isSuccess()) - .isTrue(); - } - - @Test - void doubleInRuleValueShape() throws ValidationException { - // 0.0 not in [1.5, 2.5]. - ExampleDoubleIn msg = ExampleDoubleIn.newBuilder().setVal(0.0).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - Violation v = result.getViolations().get(0); - assertThat(v.toProto().getRuleId()).isEqualTo("double.in"); - - // For in-list violations the rule_value is the in list itself, not the failing field value. - Violation.FieldValue ruleValue = v.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo(Arrays.asList(1.5, 2.5)); - FieldDescriptor expectedDesc = - DoubleRules.getDescriptor().findFieldByNumber(DoubleRules.IN_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void floatFiniteRuleValueShape() throws ValidationException { - ExampleFloatFinite msg = - ExampleFloatFinite.newBuilder().setVal(Float.POSITIVE_INFINITY).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - Violation v = result.getViolations().get(0); - assertThat(v.toProto().getRuleId()).isEqualTo("float.finite"); - - // For finite violations the rule_value is the rule's boolean, not the failing field value. - Violation.FieldValue ruleValue = v.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo(true); - FieldDescriptor expectedDesc = - FloatRules.getDescriptor().findFieldByNumber(FloatRules.FINITE_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void nativeAndCelProduceEqualViolationProtos() throws ValidationException { - // Same input, both modes — toProto() must match exactly. Excludes rule_value since that's - // not in the proto. - ExampleInt32GtLt msg = ExampleInt32GtLt.newBuilder().setVal(0).build(); - - Validator nativeV = nativeValidator(); - Validator celV = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - - assertThat(nativeV.validate(msg).getViolations().get(0).toProto()) - .isEqualTo(celV.validate(msg).getViolations().get(0).toProto()); - } -} diff --git a/src/test/java/build/buf/protovalidate/RepeatedAndMapRulesEvaluatorTest.java b/src/test/java/build/buf/protovalidate/RepeatedAndMapRulesEvaluatorTest.java deleted file mode 100644 index 78a1d2ae6..000000000 --- a/src/test/java/build/buf/protovalidate/RepeatedAndMapRulesEvaluatorTest.java +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.noimports.validationtest.ExampleMapMinMax; -import com.example.noimports.validationtest.ExampleRepeatedMinMax; -import com.example.noimports.validationtest.ExampleRepeatedUnique; -import org.junit.jupiter.api.Test; - -/** Validator-level tests for {@link RepeatedRulesEvaluator} and {@link MapRulesEvaluator}. */ -class RepeatedAndMapRulesEvaluatorTest { - - private static Validator nativeValidator() { - Config config = Config.newBuilder().setEnableNativeRules(true).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void repeatedMinItemsViolation() throws ValidationException { - // 1 item, min_items=2. - ExampleRepeatedMinMax msg = ExampleRepeatedMinMax.newBuilder().addVal(1).build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("repeated.min_items"); - assertThat(proto.getMessage()).isEqualTo("must contain at least 2 item(s)"); - } - - @Test - void repeatedMaxItemsViolation() throws ValidationException { - // 6 items, max_items=5. - ExampleRepeatedMinMax msg = - ExampleRepeatedMinMax.newBuilder() - .addVal(1) - .addVal(2) - .addVal(3) - .addVal(4) - .addVal(5) - .addVal(6) - .build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("repeated.max_items"); - assertThat(proto.getMessage()).isEqualTo("must contain no more than 5 item(s)"); - } - - @Test - void repeatedUniqueValid() throws ValidationException { - ExampleRepeatedUnique msg = - ExampleRepeatedUnique.newBuilder().addVal("a").addVal("b").addVal("c").build(); - assertThat(nativeValidator().validate(msg).isSuccess()).isTrue(); - } - - @Test - void repeatedUniqueViolation() throws ValidationException { - ExampleRepeatedUnique msg = - ExampleRepeatedUnique.newBuilder().addVal("a").addVal("b").addVal("a").build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("repeated.unique"); - assertThat(proto.getMessage()).isEqualTo("repeated value must contain unique items"); - } - - @Test - void mapMinPairsViolation() throws ValidationException { - // Empty map, min_pairs=1. - ExampleMapMinMax msg = ExampleMapMinMax.newBuilder().build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("map.min_pairs"); - assertThat(proto.getMessage()).isEqualTo("map must be at least 1 entries"); - } - - @Test - void mapMaxPairsViolation() throws ValidationException { - // 4 entries, max_pairs=3. - ExampleMapMinMax msg = - ExampleMapMinMax.newBuilder() - .putVal("a", "1") - .putVal("b", "2") - .putVal("c", "3") - .putVal("d", "4") - .build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("map.max_pairs"); - assertThat(proto.getMessage()).isEqualTo("map must be at most 3 entries"); - } - - @Test - void nativeAndCelProduceEqualViolationProto() throws ValidationException { - // Repeated unique violation in both modes. - ExampleRepeatedUnique msg = ExampleRepeatedUnique.newBuilder().addVal("a").addVal("a").build(); - Validator nativeV = nativeValidator(); - Validator celV = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - assertThat(nativeV.validate(msg).getViolations().get(0).toProto()) - .isEqualTo(celV.validate(msg).getViolations().get(0).toProto()); - } -} diff --git a/src/test/java/build/buf/protovalidate/ResidualClearingTest.java b/src/test/java/build/buf/protovalidate/ResidualClearingTest.java deleted file mode 100644 index 524cbcd58..000000000 --- a/src/test/java/build/buf/protovalidate/ResidualClearingTest.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.noimports.validationtest.ExampleBoolConst; -import com.example.noimports.validationtest.ExampleBytesConst; -import com.example.noimports.validationtest.ExampleEnumConst; -import com.example.noimports.validationtest.ExampleInt32Const; -import com.example.noimports.validationtest.ExampleMapMinMax; -import com.example.noimports.validationtest.ExampleRepeatedMinMax; -import com.example.noimports.validationtest.ExampleStringConst; -import com.google.protobuf.Message; -import org.junit.jupiter.api.Test; - -/** - * Residual-clearing contract tests. When a native evaluator handles a rule, the dispatcher must - * clear that rule on the residual {@code FieldRules} so {@code RuleCache} doesn't compile a CEL - * program that fires a duplicate violation. Each test below uses a fixture that fails exactly one - * native rule and asserts that the validator produces exactly one violation, not two. - */ -class ResidualClearingTest { - - private final Validator nativeValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(true).build()) - .build(); - - @Test - void boolConstFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleBoolConst.newBuilder().setFlag(false).build()); - } - - @Test - void int32ConstFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleInt32Const.newBuilder().setVal(0).build()); - } - - @Test - void enumConstFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleEnumConst.newBuilder().build()); - } - - @Test - void bytesConstFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleBytesConst.newBuilder().build()); - } - - @Test - void stringConstFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleStringConst.newBuilder().setVal("nope").build()); - } - - @Test - void repeatedMinItemsFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleRepeatedMinMax.newBuilder().build()); - } - - @Test - void mapMinPairsFiresOnce() throws ValidationException { - assertExactlyOneViolation(ExampleMapMinMax.newBuilder().build()); - } - - private void assertExactlyOneViolation(Message msg) throws ValidationException { - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.getViolations()) - .as( - "native dispatcher must clear the rule from the residual; expected exactly one " - + "violation but got: %s", - result.getViolations()) - .hasSize(1); - } -} diff --git a/src/test/java/build/buf/protovalidate/StringRulesEvaluatorTest.java b/src/test/java/build/buf/protovalidate/StringRulesEvaluatorTest.java deleted file mode 100644 index 1f2b53926..000000000 --- a/src/test/java/build/buf/protovalidate/StringRulesEvaluatorTest.java +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.StringRules; -import com.example.noimports.validationtest.ExampleStringConst; -import com.example.noimports.validationtest.ExampleStringEmail; -import com.example.noimports.validationtest.ExampleStringHostAndPort; -import com.example.noimports.validationtest.ExampleStringMinMaxLen; -import com.example.noimports.validationtest.HttpHeaderName; -import com.google.protobuf.Descriptors.FieldDescriptor; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -/** Validator-level tests for {@link StringRulesEvaluator}. */ -class StringRulesEvaluatorTest { - - private static Validator nativeValidator() { - Config config = Config.newBuilder().setEnableNativeRules(true).build(); - return ValidatorFactory.newBuilder().withConfig(config).build(); - } - - @Test - void stringConstFailsAndCarriesExpectedShape() throws ValidationException { - ExampleStringConst msg = ExampleStringConst.newBuilder().setVal("nope").build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - Violation v = result.getViolations().get(0); - - build.buf.validate.Violation proto = v.toProto(); - assertThat(proto.getRuleId()).isEqualTo("string.const"); - assertThat(proto.getMessage()).isEqualTo("must equal `abcd`"); - - Violation.FieldValue ruleValue = v.getRuleValue(); - assertThat(ruleValue).isNotNull(); - assertThat(ruleValue.getValue()).isEqualTo("abcd"); - FieldDescriptor expectedDesc = - StringRules.getDescriptor().findFieldByNumber(StringRules.CONST_FIELD_NUMBER); - assertThat(ruleValue.getDescriptor()).isEqualTo(expectedDesc); - } - - @Test - void emailWellKnownAcceptsValidAndRejectsInvalid() throws ValidationException { - Validator v = nativeValidator(); - assertThat( - v.validate(ExampleStringEmail.newBuilder().setVal("alice@example.com").build()) - .isSuccess()) - .isTrue(); - assertThat( - v.validate(ExampleStringEmail.newBuilder().setVal("not-an-email").build()).isSuccess()) - .isFalse(); - } - - @Test - void emailWellKnownReportsEmptyVariant() throws ValidationException { - // Empty string fires the *_empty variant rule id. - ExampleStringEmail msg = ExampleStringEmail.newBuilder().setVal("").build(); - ValidationResult result = nativeValidator().validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("string.email_empty"); - assertThat(proto.getMessage()).contains("value is empty"); - } - - @Test - void minMaxLenAppliesCharacterCounts() throws ValidationException { - Validator v = nativeValidator(); - // min_len=2, max_len=5 - assertThat(v.validate(ExampleStringMinMaxLen.newBuilder().setVal("ab").build()).isSuccess()) - .isTrue(); - assertThat(v.validate(ExampleStringMinMaxLen.newBuilder().setVal("abcde").build()).isSuccess()) - .isTrue(); - // 1 character — too short. - ValidationResult tooShort = v.validate(ExampleStringMinMaxLen.newBuilder().setVal("a").build()); - assertThat(tooShort.getViolations()).hasSize(1); - assertThat(tooShort.getViolations().get(0).toProto().getRuleId()).isEqualTo("string.min_len"); - // 6 characters — too long. - ValidationResult tooLong = - v.validate(ExampleStringMinMaxLen.newBuilder().setVal("abcdef").build()); - assertThat(tooLong.getViolations()).hasSize(1); - assertThat(tooLong.getViolations().get(0).toProto().getRuleId()).isEqualTo("string.max_len"); - } - - @Test - void minLenCountsCodePointsNotJavaChars() throws ValidationException { - // Each emoji is a single code point but two Java chars (surrogate pair). min_len=2 should - // count code points, not chars. - Validator v = nativeValidator(); - assertThat(v.validate(ExampleStringMinMaxLen.newBuilder().setVal("😀😀").build()).isSuccess()) - .isTrue(); - // One emoji = 1 code point, fails min_len=2. - assertThat(v.validate(ExampleStringMinMaxLen.newBuilder().setVal("😀").build()).isSuccess()) - .isFalse(); - } - - @Test - void hostAndPortAcceptsValidAndRejectsInvalid() throws ValidationException { - Validator v = nativeValidator(); - assertThat( - v.validate(ExampleStringHostAndPort.newBuilder().setVal("example.com:8080").build()) - .isSuccess()) - .isTrue(); - assertThat( - v.validate(ExampleStringHostAndPort.newBuilder().setVal("not-a-host-and-port").build()) - .isSuccess()) - .isFalse(); - } - - @Test - void nativeAndCelProduceEqualViolationProto() throws ValidationException { - ExampleStringConst msg = ExampleStringConst.newBuilder().setVal("nope").build(); - Validator nativeV = nativeValidator(); - Validator celV = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(false).build()) - .build(); - assertThat(nativeV.validate(msg).getViolations().get(0).toProto()) - .isEqualTo(celV.validate(msg).getViolations().get(0).toProto()); - } - - @ParameterizedTest - @ValueSource( - strings = { - "Content-Type", - "Content-Length", - "Accept", - "User-Agent", - "X-Forwarded-For", - "WWW-Authenticate", - "If-None-Match", - "Cache-Control", - "Set-Cookie", - "ETag", - ":method", - ":path", - ":status", - ":authority", - ":scheme", - "!", - "#", - "$", - "%", - "&", - "'", - "*", - "+", - "-", - ".", - "^", - "_", - "`", - "|", - "~", - "a", - "0", - ":a", - "A1!#$%&'*+-.^_|~`", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "MiXeDcAsE" - }) - void headerNameRegexValidationGood(String headerName) throws ValidationException { - HttpHeaderName msg = HttpHeaderName.newBuilder().setVal(headerName).build(); - Validator v = nativeValidator(); - assertThat(v.validate(msg).isSuccess()).isTrue(); - } - - @ParameterizedTest - @ValueSource( - strings = { - "", - ":", - " ", - "Content-Type ", - " Content-Type", - "Content Type", - "\tContent-Type", - "Content:Type", - "Content/Type", - "Content\\Type", - "Content,Type", - "Content;Type", - "Content=Type", - "Content(Type)", - "Content[Type]", - "Content{Type}", - "Content", - "Content\"Type", - "Content?Type", - "Content@Type", - "::method", - "method:", - ":method:extra", - "Conténg-Type", - "内容类型", - "Header™", - "naïve", - "Content\000Type", - "Content\177Type", - "Content\nType", - "Content\rType", - "Valid-Name\nAnother-Name", - }) - void headerNameRegexValidationBad(String headerName) throws ValidationException { - HttpHeaderName msg = HttpHeaderName.newBuilder().setVal(headerName).build(); - Validator v = nativeValidator(); - assertThat(v.validate(msg).isSuccess()).isFalse(); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidationResultTest.java b/src/test/java/build/buf/protovalidate/ValidationResultTest.java deleted file mode 100644 index 8ac014911..000000000 --- a/src/test/java/build/buf/protovalidate/ValidationResultTest.java +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.validate.FieldPathElement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Test; - -class ValidationResultTest { - @Test - void testToStringNoViolations() { - - List violations = new ArrayList<>(); - ValidationResult result = new ValidationResult(violations); - - assertThat(result.toString()).isEqualTo("Validation OK"); - assertThat(result.isSuccess()).isTrue(); - } - - @Test - void testToStringSingleViolation() { - FieldPathElement elem = - FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("test_field_name").build(); - - RuleViolation violation = - RuleViolation.newBuilder() - .setRuleId("int32.const") - .setMessage("must equal 42") - .addFirstFieldPathElement(elem) - .build(); - ValidationResult result = new ValidationResult(Collections.singletonList(violation)); - - assertThat(result.toString()) - .isEqualTo("Validation error:\n - test_field_name: must equal 42 [int32.const]"); - } - - @Test - void testToStringMultipleViolations() { - FieldPathElement elem = - FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("test_field_name").build(); - - RuleViolation violation1 = - RuleViolation.newBuilder() - .setRuleId("int32.const") - .setMessage("must equal 42") - .addFirstFieldPathElement(elem) - .build(); - - RuleViolation violation2 = - RuleViolation.newBuilder() - .setRuleId("int32.required") - .setMessage("value is required") - .addFirstFieldPathElement(elem) - .build(); - ValidationResult result = new ValidationResult(Arrays.asList(violation1, violation2)); - - assertThat(result.toString()) - .isEqualTo( - "Validation error:\n - test_field_name: must equal 42 [int32.const]\n - test_field_name: value is required [int32.required]"); - } - - @Test - void testToStringSingleViolationMultipleFieldPathElements() { - FieldPathElement elem1 = - FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("test_field_name").build(); - FieldPathElement elem2 = - FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("nested_name").build(); - - RuleViolation violation1 = - RuleViolation.newBuilder() - .setRuleId("int32.const") - .setMessage("must equal 42") - .addAllFieldPathElements(Arrays.asList(elem1, elem2)) - .build(); - - ValidationResult result = new ValidationResult(Collections.singletonList(violation1)); - - assertThat(result.toString()) - .isEqualTo( - "Validation error:\n - test_field_name.nested_name: must equal 42 [int32.const]"); - } - - @Test - void testToStringSingleViolationNoFieldPathElements() { - RuleViolation violation = - RuleViolation.newBuilder().setRuleId("int32.const").setMessage("must equal 42").build(); - ValidationResult result = new ValidationResult(Collections.singletonList(violation)); - - assertThat(result.toString()).isEqualTo("Validation error:\n - must equal 42 [int32.const]"); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java b/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java deleted file mode 100644 index 515e6e089..000000000 --- a/src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.validate.FieldPath; -import build.buf.validate.FieldRules; -import build.buf.validate.Violation; -import com.example.imports.buf.validate.RepeatedRules; -import java.util.Arrays; -import org.junit.jupiter.api.Test; - -/** This test verifies that custom (CEL-based) field and/or message rules evaluate as expected. */ -public class ValidatorCelExpressionTest { - - @Test - public void testFieldExpressionRepeatedMessage() throws Exception { - // Nested message wrapping the int 1 - com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg one = - com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg.newBuilder() - .setA(1) - .build(); - - // Nested message wrapping the int 2 - com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg two = - com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg.newBuilder() - .setA(2) - .build(); - - // Create a valid message (1, 1) - com.example.imports.validationtest.FieldExpressionRepeatedMessage validMsg = - com.example.imports.validationtest.FieldExpressionRepeatedMessage.newBuilder() - .addAllVal(Arrays.asList(one, one)) - .build(); - - // Create an invalid message (1, 2, 1) - com.example.imports.validationtest.FieldExpressionRepeatedMessage invalidMsg = - com.example.imports.validationtest.FieldExpressionRepeatedMessage.newBuilder() - .addAllVal(Arrays.asList(one, two, one)) - .build(); - - // Build a model of the expected violation - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - invalidMsg.getDescriptorForType().findFieldByName("val")) - .toBuilder() - .build())) - .setRule( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.CEL_FIELD_NUMBER)) - .toBuilder() - .setIndex(0) - .build())) - .setRuleId("field_expression.repeated.message") - .setMessage("test message field_expression.repeated.message") - .build(); - - Validator validator = ValidatorFactory.newBuilder().build(); - - // Valid message checks - ValidationResult validResult = validator.validate(validMsg); - assertThat(validResult.isSuccess()).isTrue(); - - // Invalid message checks - ValidationResult invalidResult = validator.validate(invalidMsg); - assertThat(invalidResult.isSuccess()).isFalse(); - assertThat(invalidResult.toProto().getViolationsList()).containsExactly(expectedViolation); - } - - @Test - public void testFieldExpressionRepeatedMessageItems() throws Exception { - // Nested message wrapping the int 1 - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg one = - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg.newBuilder() - .setA(1) - .build(); - - // Nested message wrapping the int 2 - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg two = - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg.newBuilder() - .setA(2) - .build(); - - // Create a valid message (1, 1) - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems validMsg = - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.newBuilder() - .addAllVal(Arrays.asList(one, one)) - .build(); - - // Create an invalid message (1, 2, 1) - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems invalidMsg = - com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.newBuilder() - .addAllVal(Arrays.asList(one, two, one)) - .build(); - - // Build a model of the expected violation - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - invalidMsg.getDescriptorForType().findFieldByName("val")) - .toBuilder() - .setIndex(1) - .build())) - .setRule( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.REPEATED_FIELD_NUMBER))) - .addElements( - FieldPathUtils.fieldPathElement( - RepeatedRules.getDescriptor().findFieldByName("items"))) - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.CEL_FIELD_NUMBER)) - .toBuilder() - .setIndex(0) - .build())) - .setRuleId("field_expression.repeated.message.items") - .setMessage("test message field_expression.repeated.message.items") - .build(); - - Validator validator = ValidatorFactory.newBuilder().build(); - - // Valid message checks - ValidationResult validResult = validator.validate(validMsg); - assertThat(validResult.isSuccess()).isTrue(); - - // Invalid message checks - ValidationResult invalidResult = validator.validate(invalidMsg); - assertThat(invalidResult.isSuccess()).isFalse(); - assertThat(invalidResult.toProto().getViolationsList()).containsExactly(expectedViolation); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidatorConstructionTest.java b/src/test/java/build/buf/protovalidate/ValidatorConstructionTest.java deleted file mode 100644 index 635019e6b..000000000 --- a/src/test/java/build/buf/protovalidate/ValidatorConstructionTest.java +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.imports.validationtest.ExampleFieldRules; -import com.example.imports.validationtest.FieldExpressionMapInt32; -import com.example.imports.validationtest.FieldExpressionMultiple; -import com.google.protobuf.Descriptors.Descriptor; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; - -public class ValidatorConstructionTest { - - // Tests validation works as planned with default builder. - @Test - public void testDefaultBuilder() throws ValidationException { - Map testMap = new HashMap<>(); - testMap.put(42, 42); - FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); - - Validator validator = ValidatorFactory.newBuilder().build(); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("all map values must equal 1"); - } - - // Tests validation works as planned with default builder and config - @Test - public void testDefaultBuilderWithConfig() throws ValidationException { - Map testMap = new HashMap<>(); - testMap.put(42, 42); - FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); - - Config cfg = Config.newBuilder().setFailFast(true).build(); - Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("all map values must equal 1"); - } - - // Tests that if the correct seed descriptors are provided and lazy is disabled, - // validation works as planned. - @Test - public void testSeedDescriptorsLazyDisabled() throws ValidationException { - Map testMap = new HashMap<>(); - testMap.put(42, 42); - FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); - - List seedDescriptors = new ArrayList<>(); - FieldExpressionMapInt32 reg = FieldExpressionMapInt32.newBuilder().build(); - seedDescriptors.add(reg.getDescriptorForType()); - - Config cfg = Config.newBuilder().setFailFast(true).build(); - - // Note that buildWithDescriptors throws the exception so the validator builder - // can be created ahead of time without having to catch an exception. - ValidatorFactory.ValidatorBuilder bldr = ValidatorFactory.newBuilder().withConfig(cfg); - Validator validator = bldr.buildWithDescriptors(seedDescriptors, true); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("all map values must equal 1"); - } - - // Tests that the seed descriptor list is immutable inside the validator and that if - // a descriptor is removed after the validator is created, validation still works as planned. - @Test - public void testSeedDescriptorsImmutable() throws ValidationException { - Map testMap = new HashMap<>(); - testMap.put(42, 42); - FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); - - List seedDescriptors = new ArrayList<>(); - seedDescriptors.add(msg.getDescriptorForType()); - - Config cfg = Config.newBuilder().setFailFast(true).build(); - Validator validator = - ValidatorFactory.newBuilder().withConfig(cfg).buildWithDescriptors(seedDescriptors, true); - - // Remove descriptor from list after the validator is created to verify validation still works - seedDescriptors.clear(); - - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("all map values must equal 1"); - } - - // Tests that if a message is attempted to be validated and it wasn't in the initial - // list of seed descriptors AND lazy is disabled, that a message is returned that - // no evaluator is available. - @Test - public void testSeedDescriptorsWithWrongDescriptorAndLazyDisabled() throws ValidationException { - Map testMap = new HashMap<>(); - testMap.put(42, 42); - FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); - - List seedDescriptors = new ArrayList<>(); - ExampleFieldRules wrong = ExampleFieldRules.newBuilder().build(); - seedDescriptors.add(wrong.getDescriptorForType()); - - Config cfg = Config.newBuilder().setFailFast(true).build(); - Validator validator = - ValidatorFactory.newBuilder().withConfig(cfg).buildWithDescriptors(seedDescriptors, true); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("No evaluator available for " + msg.getDescriptorForType().getFullName()); - } - - // Tests that an IllegalStateException is thrown if an empty descriptor list is given - // and lazy is disabled. - @Test - public void testEmptySeedDescriptorsInvalidState() { - List seedDescriptors = new ArrayList<>(); - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy( - () -> ValidatorFactory.newBuilder().buildWithDescriptors(seedDescriptors, true)); - } - - // Tests that an IllegalStateException is thrown if a null descriptor list is given - // and lazy is disabled. - @Test - public void testNullSeedDescriptorsInvalidState() { - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy(() -> ValidatorFactory.newBuilder().buildWithDescriptors(null, true)); - } - - // Tests that when an empty list of seed descriptors is provided and lazy is enabled - // that the missing message descriptor is successfully built and validation works as planned. - @Test - public void testEmptySeedDescriptorsLazyEnabled() throws ValidationException { - Map testMap = new HashMap<>(); - testMap.put(42, 42); - FieldExpressionMapInt32 msg = FieldExpressionMapInt32.newBuilder().putAllVal(testMap).build(); - - List seedDescriptors = new ArrayList<>(); - Config cfg = Config.newBuilder().setFailFast(true).build(); - Validator validator = - ValidatorFactory.newBuilder().withConfig(cfg).buildWithDescriptors(seedDescriptors, false); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("all map values must equal 1"); - } - - // Tests that when a null list of seed descriptors is provided, a NullPointerException - // is thrown with a message that descriptors cannot be null. - @Test - public void testNullSeedDescriptorsLazyEnabled() { - assertThatExceptionOfType(NullPointerException.class) - .isThrownBy(() -> ValidatorFactory.newBuilder().buildWithDescriptors(null, false)) - .withMessageContaining("descriptors must not be null"); - } - - // Tests that the config is applied when building a validator. - @Test - public void testConfigApplied() throws ValidationException { - // Value must be at most 5 characters and must be lowercase alpha chars or numbers. - FieldExpressionMultiple msg = FieldExpressionMultiple.newBuilder().setVal("INVALID").build(); - - // Set fail fast to true, so we exit after the first validation failure. - Config cfg = Config.newBuilder().setFailFast(true).build(); - Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("must be at most 5 characters"); - } - - // Tests that the config is applied when building a validator with seed descriptors. - @Test - public void testConfigAppliedWithSeedDescriptors() throws ValidationException { - // Value must be at most 5 characters and must be lowercase alpha chars or numbers. - FieldExpressionMultiple msg = FieldExpressionMultiple.newBuilder().setVal("INVALID").build(); - - FieldExpressionMultiple desc = FieldExpressionMultiple.newBuilder().build(); - List seedDescriptors = new ArrayList<>(); - seedDescriptors.add(desc.getDescriptorForType()); - - // Set fail fast to true, so we exit after the first validation failure. - Config cfg = Config.newBuilder().setFailFast(true).build(); - Validator validator = - ValidatorFactory.newBuilder().withConfig(cfg).buildWithDescriptors(seedDescriptors, false); - ValidationResult result = validator.validate(msg); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getViolations().size()).isEqualTo(1); - assertThat(result.getViolations().get(0).toProto().getMessage()) - .isEqualTo("must be at most 5 characters"); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java b/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java deleted file mode 100644 index 3c4c77474..000000000 --- a/src/test/java/build/buf/protovalidate/ValidatorDifferentJavaPackagesTest.java +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import build.buf.validate.FieldRules; -import build.buf.validate.Violation; -import com.example.imports.buf.validate.StringRules; -import com.example.imports.validationtest.ExampleFieldRules; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; -import java.util.Collections; -import java.util.List; -import org.junit.jupiter.api.Test; - -/** - * protovalidate-java contains protoc generated classes for the bufbuild/protovalidate module. In some cases - * however, using buf generate in managed mode (without except) or with remote - * packages will result in the generated code for this module being created in another package. - * While not desirable, we shouldn't fail with an exception in this case and should make a - * best-effort attempt to load the validation rules and validate them. - * - *

Prior to the fix, calling validate would fail up front with: - * - *

IllegalArgumentException: mergeFrom(Message) can only merge messages of the same type.
- * - * For the tests in this class, we've generated java code to two separate packages for the module - * found in src/test/resources/proto. - * - *
    - *
  1. com.example.imports.* is generated with managed mode and - * --include-imports, so it contains references to the bufbuild/protovalidate - * extensions under com.example.imports.buf.validate.* - *
  2. com.example.noimports.* is generated with managed mode and an exception for - * buf.build/bufbuild/protovalidate, so it contains references to the - * bufbuild/protovalidate extensions under build.buf.validate.* (honoring the - * java_package option). - *
- * - * These tests ensure that the same classes can be validated identically regardless of where the - * protovalidate extensions are found. - */ -public class ValidatorDifferentJavaPackagesTest { - @Test - public void testValidationFieldRules() throws Exception { - // Valid message - matches regex - com.example.imports.validationtest.ExampleFieldRules validMsgImports = - com.example.imports.validationtest.ExampleFieldRules.newBuilder() - .setRegexStringField("abc123") - .build(); - expectNoViolations(validMsgImports); - - // Create same message under noimports package. Validation behavior should match. - com.example.noimports.validationtest.ExampleFieldRules validMsgNoImports = - com.example.noimports.validationtest.ExampleFieldRules.parseFrom( - validMsgImports.toByteString()); - expectNoViolations(validMsgNoImports); - - // 10 chars long - regex requires 1-9 chars - com.example.imports.validationtest.ExampleFieldRules invalidMsgImports = - com.example.imports.validationtest.ExampleFieldRules.newBuilder() - .setRegexStringField("0123456789") - .build(); - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - com.example.imports.validationtest.ExampleFieldRules.getDescriptor() - .findFieldByNumber( - ExampleFieldRules.REGEX_STRING_FIELD_FIELD_NUMBER)))) - .setRule( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.STRING_FIELD_NUMBER))) - .addElements( - FieldPathUtils.fieldPathElement( - StringRules.getDescriptor() - .findFieldByNumber(StringRules.PATTERN_FIELD_NUMBER)))) - .setRuleId("string.pattern") - .setMessage("does not match regex pattern `^[a-z0-9]{1,9}$`") - .build(); - expectViolation(invalidMsgImports, expectedViolation); - - // Create same message under noimports package. Validation behavior should match. - com.example.noimports.validationtest.ExampleFieldRules invalidMsgNoImports = - com.example.noimports.validationtest.ExampleFieldRules.newBuilder() - .setRegexStringField("0123456789") - .build(); - expectViolation(invalidMsgNoImports, expectedViolation); - } - - @Test - public void testValidationOneofRules() - throws ValidationException, InvalidProtocolBufferException { - // Valid message - matches oneof rule - com.example.imports.validationtest.ExampleOneofRules validMsgImports = - com.example.imports.validationtest.ExampleOneofRules.newBuilder() - .setEmail("foo@bar.com") - .build(); - expectNoViolations(validMsgImports); - - // Create same message under noimports package. Validation behavior should match. - com.example.noimports.validationtest.ExampleOneofRules validMsgNoImports = - com.example.noimports.validationtest.ExampleOneofRules.parseFrom( - validMsgImports.toByteString()); - expectNoViolations(validMsgNoImports); - - com.example.imports.validationtest.ExampleOneofRules invalidMsgImports = - com.example.imports.validationtest.ExampleOneofRules.getDefaultInstance(); - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements(FieldPathElement.newBuilder().setFieldName("contact_info"))) - .setRuleId("required") - .setMessage("exactly one field is required in oneof") - .build(); - expectViolation(invalidMsgImports, expectedViolation); - - // Create same message under noimports package. Validation behavior should match. - com.example.noimports.validationtest.ExampleOneofRules invalidMsgNoImports = - com.example.noimports.validationtest.ExampleOneofRules.parseFrom( - invalidMsgImports.toByteString()); - expectViolation(invalidMsgNoImports, expectedViolation); - } - - @Test - public void testValidationMessageRulesDifferentJavaPackage() throws Exception { - com.example.imports.validationtest.ExampleMessageRules validMsg = - com.example.imports.validationtest.ExampleMessageRules.newBuilder() - .setPrimaryEmail("foo@bar.com") - .build(); - expectNoViolations(validMsg); - - // Create same message under noimports package. Validation behavior should match. - com.example.noimports.validationtest.ExampleMessageRules validMsgNoImports = - com.example.noimports.validationtest.ExampleMessageRules.parseFrom(validMsg.toByteString()); - expectNoViolations(validMsgNoImports); - - com.example.imports.validationtest.ExampleMessageRules invalidMsgImports = - com.example.imports.validationtest.ExampleMessageRules.newBuilder() - .setSecondaryEmail("foo@bar.com") - .build(); - Violation expectedViolation = - Violation.newBuilder() - .setRuleId("secondary_email_depends_on_primary") - .setMessage("cannot set a secondary email without setting a primary one") - .build(); - expectViolation(invalidMsgImports, expectedViolation); - - // Create same message under noimports package. Validation behavior should match. - com.example.noimports.validationtest.ExampleMessageRules invalidMsgNoImports = - com.example.noimports.validationtest.ExampleMessageRules.parseFrom( - invalidMsgImports.toByteString()); - expectViolation(invalidMsgNoImports, expectedViolation); - } - - private void expectNoViolations(Message msg) throws ValidationException { - expectViolations(msg, Collections.emptyList()); - } - - private void expectViolation(Message msg, Violation violation) throws ValidationException { - expectViolations(msg, Collections.singletonList(violation)); - } - - private void expectViolations(Message msg, List expected) throws ValidationException { - Validator validator = ValidatorFactory.newBuilder().build(); - List violations = validator.validate(msg).toProto().getViolationsList(); - assertThat(violations).containsExactlyInAnyOrderElementsOf(expected); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java b/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java deleted file mode 100644 index 31149c6a5..000000000 --- a/src/test/java/build/buf/protovalidate/ValidatorDynamicMessageTest.java +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static com.example.imports.validationtest.PredefinedProto.isIdent; -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.validate.FieldPath; -import build.buf.validate.FieldPathElement; -import build.buf.validate.FieldRules; -import build.buf.validate.Violation; -import com.example.imports.buf.validate.StringRules; -import com.example.imports.validationtest.ExamplePredefinedFieldRules; -import com.example.noimports.validationtest.ExampleFieldRules; -import com.example.noimports.validationtest.ExampleMessageRules; -import com.example.noimports.validationtest.ExampleOneofRules; -import com.example.noimports.validationtest.ExampleRequiredFieldRules; -import com.example.noimports.validationtest.PredefinedProto; -import com.google.protobuf.DescriptorProtos; -import com.google.protobuf.Descriptors; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; -import com.google.protobuf.TypeRegistry; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.junit.jupiter.api.Test; - -/** - * This test mimics the behavior when performing validation with protovalidate on a file descriptor - * set (as created by protoc --retain_options --descriptor_set_out=...). These - * descriptor types have the protovalidate extensions as unknown fields and need to be parsed with - * an extension registry for the rules to be recognized and validated. - */ -public class ValidatorDynamicMessageTest { - - @Test - public void testFieldRuleDynamicMessage() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExampleFieldRules.getDefaultInstance()); - messageBuilder.setField( - messageBuilder.getDescriptorForType().findFieldByName("regex_string_field"), "0123456789"); - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - messageBuilder - .getDescriptorForType() - .findFieldByName("regex_string_field")))) - .setRule( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.STRING_FIELD_NUMBER))) - .addElements( - FieldPathUtils.fieldPathElement( - StringRules.getDescriptor() - .findFieldByNumber(StringRules.PATTERN_FIELD_NUMBER)))) - .setRuleId("string.pattern") - .setMessage("does not match regex pattern `^[a-z0-9]{1,9}$`") - .build(); - ValidationResult result = - ValidatorFactory.newBuilder().build().validate(messageBuilder.build()); - assertThat(result.toProto().getViolationsList()).containsExactly(expectedViolation); - assertThat(result.getViolations().get(0).getFieldValue().getValue()).isEqualTo("0123456789"); - assertThat(result.getViolations().get(0).getRuleValue().getValue()) - .isEqualTo("^[a-z0-9]{1,9}$"); - } - - @Test - public void testOneofRuleDynamicMessage() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExampleOneofRules.getDefaultInstance()); - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements(FieldPathElement.newBuilder().setFieldName("contact_info"))) - .setRuleId("required") - .setMessage("exactly one field is required in oneof") - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(messageBuilder.build()) - .toProto() - .getViolationsList()) - .containsExactly(expectedViolation); - } - - @Test - public void testMessageRuleDynamicMessage() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExampleMessageRules.getDefaultInstance()); - messageBuilder.setField( - messageBuilder.getDescriptorForType().findFieldByName("secondary_email"), - "something@somewhere.com"); - Violation expectedViolation = - Violation.newBuilder() - .setRuleId("secondary_email_depends_on_primary") - .setMessage("cannot set a secondary email without setting a primary one") - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(messageBuilder.build()) - .toProto() - .getViolationsList()) - .containsExactly(expectedViolation); - } - - @Test - public void testRequiredFieldRuleDynamicMessage() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExampleRequiredFieldRules.getDefaultInstance()); - messageBuilder.setField( - messageBuilder.getDescriptorForType().findFieldByName("regex_string_field"), "abc123"); - assertThat( - ValidatorFactory.newBuilder().build().validate(messageBuilder.build()).getViolations()) - .isEmpty(); - } - - @Test - public void testRequiredFieldRuleDynamicMessageInvalid() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExampleRequiredFieldRules.getDefaultInstance()); - messageBuilder.setField( - messageBuilder.getDescriptorForType().findFieldByName("regex_string_field"), "0123456789"); - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - messageBuilder - .getDescriptorForType() - .findFieldByName("regex_string_field")))) - .setRule( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.STRING_FIELD_NUMBER))) - .addElements( - FieldPathUtils.fieldPathElement( - StringRules.getDescriptor() - .findFieldByNumber(StringRules.PATTERN_FIELD_NUMBER)))) - .setRuleId("string.pattern") - .setMessage("does not match regex pattern `^[a-z0-9]{1,9}$`") - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(messageBuilder.build()) - .toProto() - .getViolationsList()) - .containsExactly(expectedViolation); - } - - @Test - public void testPredefinedFieldRuleDynamicMessage() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExamplePredefinedFieldRules.getDefaultInstance()); - messageBuilder.setField( - messageBuilder.getDescriptorForType().findFieldByName("ident_field"), "abc123"); - ExtensionRegistry registry = ExtensionRegistry.newInstance(); - registry.add(isIdent); - TypeRegistry typeRegistry = - TypeRegistry.newBuilder().add(isIdent.getDescriptor().getContainingType()).build(); - Config config = - Config.newBuilder().setExtensionRegistry(registry).setTypeRegistry(typeRegistry).build(); - assertThat( - ValidatorFactory.newBuilder() - .withConfig(config) - .build() - .validate(messageBuilder.build()) - .getViolations()) - .isEmpty(); - } - - @Test - public void testPredefinedFieldRuleDynamicMessageInvalid() throws Exception { - DynamicMessage.Builder messageBuilder = - createMessageWithUnknownOptions(ExamplePredefinedFieldRules.getDefaultInstance()); - messageBuilder.setField( - messageBuilder.getDescriptorForType().findFieldByName("ident_field"), "0123456789"); - Violation expectedViolation = - Violation.newBuilder() - .setField( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - messageBuilder.getDescriptorForType().findFieldByName("ident_field")))) - .setRule( - FieldPath.newBuilder() - .addElements( - FieldPathUtils.fieldPathElement( - FieldRules.getDescriptor() - .findFieldByNumber(FieldRules.STRING_FIELD_NUMBER))) - .addElements( - FieldPathUtils.fieldPathElement(PredefinedProto.isIdent.getDescriptor()))) - .setRuleId("string.is_ident") - .setMessage("invalid identifier") - .build(); - ExtensionRegistry registry = ExtensionRegistry.newInstance(); - registry.add(isIdent); - TypeRegistry typeRegistry = - TypeRegistry.newBuilder().add(isIdent.getDescriptor().getContainingType()).build(); - Config config = - Config.newBuilder().setExtensionRegistry(registry).setTypeRegistry(typeRegistry).build(); - assertThat( - ValidatorFactory.newBuilder() - .withConfig(config) - .build() - .validate(messageBuilder.build()) - .toProto() - .getViolationsList()) - .containsExactly(expectedViolation); - } - - private static void gatherDependencies( - Descriptors.FileDescriptor fd, Set dependencies) { - dependencies.add(fd.toProto()); - for (Descriptors.FileDescriptor dependency : fd.getDependencies()) { - gatherDependencies(dependency, dependencies); - } - } - - private static DescriptorProtos.FileDescriptorSet createFileDescriptorSetForMessage( - Descriptors.Descriptor message) { - DescriptorProtos.FileDescriptorSet.Builder builder = - DescriptorProtos.FileDescriptorSet.newBuilder(); - Set dependencies = new LinkedHashSet<>(); - gatherDependencies(message.getFile(), dependencies); - builder.addAllFile(dependencies); - return builder.build(); - } - - private static Descriptors.FileDescriptor getFileDescriptor( - String name, Map fds) - throws Descriptors.DescriptorValidationException { - DescriptorProtos.FileDescriptorProto fdProto = fds.get(name); - if (fdProto == null) { - throw new IllegalArgumentException("unable to file file descriptor proto: " + name); - } - Descriptors.FileDescriptor[] dependencies = - new Descriptors.FileDescriptor[fdProto.getDependencyCount()]; - for (int i = 0; i < fdProto.getDependencyCount(); i++) { - dependencies[i] = getFileDescriptor(fdProto.getDependency(i), fds); - } - return Descriptors.FileDescriptor.buildFrom(fdProto, dependencies); - } - - private static DynamicMessage.Builder createMessageWithUnknownOptions(Message message) - throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException { - DescriptorProtos.FileDescriptorSet fds = - createFileDescriptorSetForMessage(message.getDescriptorForType()); - // Reparse file descriptor set from encoded form (loses known extensions). - fds = DescriptorProtos.FileDescriptorSet.parseFrom(fds.toByteArray()); - Map fdsMap = - fds.getFileList().stream() - .collect( - Collectors.toMap( - DescriptorProtos.FileDescriptorProto::getName, Function.identity())); - Descriptors.FileDescriptor descriptor = - getFileDescriptor(message.getDescriptorForType().getFile().getName(), fdsMap); - return DynamicMessage.newBuilder( - descriptor.findMessageTypeByName(message.getDescriptorForType().getName())); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidatorImportTest.java b/src/test/java/build/buf/protovalidate/ValidatorImportTest.java deleted file mode 100644 index cb7912cb4..000000000 --- a/src/test/java/build/buf/protovalidate/ValidatorImportTest.java +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.example.imports.validationtest.ExampleImportMessage; -import com.example.imports.validationtest.ExampleImportMessageFieldRule; -import com.example.imports.validationtest.ExampleImportMessageInMap; -import com.example.imports.validationtest.ExampleImportMessageInMapFieldRule; -import com.example.imports.validationtest.ExampleImportedMessage; -import org.junit.jupiter.api.Test; - -public class ValidatorImportTest { - @Test - public void testImportedMessageFromAnotherFile() throws Exception { - com.example.imports.validationtest.ExampleImportMessage valid = - ExampleImportMessage.newBuilder() - .setImportedSubmessage( - ExampleImportedMessage.newBuilder().setHexString("0123456789abcdef").build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(valid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(0); - - com.example.imports.validationtest.ExampleImportMessage invalid = - ExampleImportMessage.newBuilder() - .setImportedSubmessage(ExampleImportedMessage.newBuilder().setHexString("zyx").build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(invalid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(1); - } - - @Test - public void testImportedMessageFromAnotherFileInField() throws Exception { - com.example.imports.validationtest.ExampleImportMessageFieldRule valid = - ExampleImportMessageFieldRule.newBuilder() - .setMessageWithImport( - ExampleImportMessage.newBuilder() - .setImportedSubmessage( - ExampleImportedMessage.newBuilder() - .setHexString("0123456789abcdef") - .build()) - .build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(valid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(0); - - com.example.imports.validationtest.ExampleImportMessageFieldRule invalid = - ExampleImportMessageFieldRule.newBuilder() - .setMessageWithImport( - ExampleImportMessage.newBuilder() - .setImportedSubmessage( - ExampleImportedMessage.newBuilder().setHexString("zyx").build()) - .build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(invalid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(1); - } - - @Test - public void testImportedMessageFromAnotherFileInMap() throws Exception { - com.example.imports.validationtest.ExampleImportMessageInMap valid = - ExampleImportMessageInMap.newBuilder() - .putImportedSubmessage( - 0, ExampleImportedMessage.newBuilder().setHexString("0123456789abcdef").build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(valid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(0); - - com.example.imports.validationtest.ExampleImportMessageInMap invalid = - ExampleImportMessageInMap.newBuilder() - .putImportedSubmessage( - 0, ExampleImportedMessage.newBuilder().setHexString("zyx").build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(invalid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(1); - } - - @Test - public void testImportedMessageFromAnotherFileInMapInField() throws Exception { - com.example.imports.validationtest.ExampleImportMessageInMapFieldRule valid = - ExampleImportMessageInMapFieldRule.newBuilder() - .setMessageWithImport( - ExampleImportMessageInMap.newBuilder() - .putImportedSubmessage( - 0, - ExampleImportedMessage.newBuilder() - .setHexString("0123456789abcdef") - .build()) - .build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(valid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(0); - - com.example.imports.validationtest.ExampleImportMessageInMapFieldRule invalid = - ExampleImportMessageInMapFieldRule.newBuilder() - .setMessageWithImport( - ExampleImportMessageInMap.newBuilder() - .putImportedSubmessage( - 0, ExampleImportedMessage.newBuilder().setHexString("zyx").build()) - .build()) - .build(); - assertThat( - ValidatorFactory.newBuilder() - .build() - .validate(invalid) - .toProto() - .getViolationsList() - .size()) - .isEqualTo(1); - } -} diff --git a/src/test/java/build/buf/protovalidate/ValidatorStringExtensionsTest.java b/src/test/java/build/buf/protovalidate/ValidatorStringExtensionsTest.java deleted file mode 100644 index 7f5f55658..000000000 --- a/src/test/java/build/buf/protovalidate/ValidatorStringExtensionsTest.java +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.Violation; -import com.example.noimports.validationtest.ExampleStringExtensions; -import org.junit.jupiter.api.Test; - -public class ValidatorStringExtensionsTest { - @Test - public void testStringExtensionsPathMatchesParent() throws ValidationException { - // Test for https://github.com/bufbuild/protovalidate-java/issues/438. - Validator validator = ValidatorFactory.newBuilder().build(); - - ExampleStringExtensions validMsg = - ExampleStringExtensions.newBuilder().setParent("foo/bar").setPath("foo/bar/baz").build(); - ValidationResult result = validator.validate(validMsg); - assertThat(result.isSuccess()).isTrue(); - - ExampleStringExtensions invalidMsg = - ExampleStringExtensions.newBuilder().setParent("foo/bar").setPath("foo/other/baz").build(); - result = validator.validate(invalidMsg); - assertThat(result.isSuccess()).isFalse(); - Violation expectedViolation = - Violation.newBuilder() - .setRuleId("path_matches_parent") - .setMessage("path must be a child of parent") - .build(); - assertThat(result.toProto().getViolationsList()).containsExactly(expectedViolation); - } -} diff --git a/conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java b/src/test/java/build/buf/protovalidate/ValidatorTest.java similarity index 64% rename from conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java rename to src/test/java/build/buf/protovalidate/ValidatorTest.java index 4f3deaab3..db7c0aa9e 100644 --- a/conformance/src/test/java/build/buf/protovalidate/ValidatorTest.java +++ b/src/test/java/build/buf/protovalidate/ValidatorTest.java @@ -1,4 +1,4 @@ -// Copyright 2023-2026 Buf Technologies, Inc. +// Copyright 2023 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,61 +15,57 @@ package build.buf.protovalidate; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import build.buf.gen.buf.validate.conformance.cases.AnEnum; +import build.buf.gen.buf.validate.conformance.cases.BoolConstTrue; +import build.buf.gen.buf.validate.conformance.cases.BytesContains; +import build.buf.gen.buf.validate.conformance.cases.DurationGTELTE; +import build.buf.gen.buf.validate.conformance.cases.Embed; +import build.buf.gen.buf.validate.conformance.cases.EnumDefined; +import build.buf.gen.buf.validate.conformance.cases.Fixed32LT; +import build.buf.gen.buf.validate.conformance.cases.Int32In; +import build.buf.gen.buf.validate.conformance.cases.MapRecursive; +import build.buf.gen.buf.validate.conformance.cases.OneofIgnoreEmpty; +import build.buf.gen.buf.validate.conformance.cases.RepeatedEnumIn; +import build.buf.gen.buf.validate.conformance.cases.RepeatedExact; +import build.buf.gen.buf.validate.conformance.cases.RepeatedExactIgnore; +import build.buf.gen.buf.validate.conformance.cases.RepeatedMin; +import build.buf.gen.buf.validate.conformance.cases.RepeatedUnique; +import build.buf.gen.buf.validate.conformance.cases.SFixed64In; +import build.buf.gen.buf.validate.conformance.cases.StringContains; +import build.buf.gen.buf.validate.conformance.cases.StringLen; +import build.buf.gen.buf.validate.conformance.cases.StringPrefix; +import build.buf.gen.buf.validate.conformance.cases.TimestampConst; +import build.buf.gen.buf.validate.conformance.cases.TimestampWithin; +import build.buf.gen.buf.validate.conformance.cases.WrapperDouble; +import build.buf.gen.buf.validate.conformance.cases.custom_constraints.DynRuntimeError; +import build.buf.gen.buf.validate.conformance.cases.custom_constraints.FieldExpressions; import build.buf.protovalidate.exceptions.ExecutionException; import build.buf.protovalidate.exceptions.ValidationException; -import build.buf.validate.conformance.cases.AnEnum; -import build.buf.validate.conformance.cases.BoolConstTrue; -import build.buf.validate.conformance.cases.BytesContains; -import build.buf.validate.conformance.cases.BytesIn; -import build.buf.validate.conformance.cases.DurationGTELTE; -import build.buf.validate.conformance.cases.DurationIn; -import build.buf.validate.conformance.cases.Embed; -import build.buf.validate.conformance.cases.EnumDefined; -import build.buf.validate.conformance.cases.Fixed32LT; -import build.buf.validate.conformance.cases.Int32In; -import build.buf.validate.conformance.cases.MapRecursive; -import build.buf.validate.conformance.cases.RepeatedEnumIn; -import build.buf.validate.conformance.cases.RepeatedExact; -import build.buf.validate.conformance.cases.RepeatedExactIgnore; -import build.buf.validate.conformance.cases.RepeatedMin; -import build.buf.validate.conformance.cases.RepeatedUnique; -import build.buf.validate.conformance.cases.SFixed64In; -import build.buf.validate.conformance.cases.StringContains; -import build.buf.validate.conformance.cases.StringLen; -import build.buf.validate.conformance.cases.StringPrefix; -import build.buf.validate.conformance.cases.TimestampConst; -import build.buf.validate.conformance.cases.TimestampWithin; -import build.buf.validate.conformance.cases.WrapperDouble; -import build.buf.validate.conformance.cases.custom_rules.DynRuntimeError; -import build.buf.validate.conformance.cases.custom_rules.FieldExpressionMultipleScalar; -import build.buf.validate.conformance.cases.custom_rules.FieldExpressionNestedScalar; -import build.buf.validate.conformance.cases.custom_rules.FieldExpressionScalar; import com.google.protobuf.ByteString; import com.google.protobuf.DoubleValue; import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import java.util.Arrays; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.Before; +import org.junit.Test; public class ValidatorTest { private Validator validator; - @BeforeEach + @Before public void setUp() { - Config config = Config.newBuilder().build(); - validator = ValidatorFactory.newBuilder().withConfig(config).build(); + Config config = Config.builder().build(); + validator = new Validator(config); } @Test public void strprefix() throws Exception { StringPrefix invalid = StringPrefix.newBuilder().setVal("foo").build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations().isEmpty()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); } @Test @@ -77,16 +73,16 @@ public void bytescontains() throws Exception { BytesContains invalid = BytesContains.newBuilder().setVal(ByteString.copyFromUtf8("candy bars")).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations().isEmpty()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); } @Test public void strcontains() throws Exception { StringContains invalid = StringContains.newBuilder().setVal("foobar").build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations().isEmpty()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); } @Test @@ -94,7 +90,7 @@ public void boolconsttrue() throws Exception { BoolConstTrue invalid = BoolConstTrue.newBuilder().build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test @@ -103,7 +99,7 @@ public void timestampwithin() throws Exception { TimestampWithin.newBuilder().setVal(Timestamp.newBuilder().build()).build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test @@ -111,8 +107,17 @@ public void timestampcost() throws Exception { TimestampConst invalid = TimestampConst.newBuilder().setVal(Timestamp.newBuilder().setSeconds(3).build()).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations().isEmpty()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); + } + + @Test + public void OneofIgnoreEmpty() throws Exception { + OneofIgnoreEmpty invalid = + OneofIgnoreEmpty.newBuilder().setY(ByteString.copyFromUtf8("")).build(); + ValidationResult validate = validator.validate(invalid); + assertThat(validate.getViolations().isEmpty()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); } @Test @@ -120,7 +125,7 @@ public void enumdefined() throws Exception { EnumDefined invalid = EnumDefined.newBuilder().setValValue(2147483647).build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test @@ -128,7 +133,7 @@ public void strictFixed32LT() throws Exception { Fixed32LT invalid = Fixed32LT.newBuilder().setVal(5).build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test @@ -137,16 +142,15 @@ public void strictWrapperDouble() throws Exception { WrapperDouble.newBuilder().setVal(DoubleValue.newBuilder().build()).build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test public void strictFieldExpressions() throws Exception { - FieldExpressionMultipleScalar invalid = - FieldExpressionMultipleScalar.newBuilder().setVal(1).build(); + FieldExpressions invalid = FieldExpressions.newBuilder().build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(2); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test @@ -157,14 +161,14 @@ public void strictDurationGTELTE() throws Exception { .build(); ValidationResult validate = validator.validate(invalid); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test public void strictRepeatedExact() throws Exception { RepeatedExact invalid = RepeatedExact.newBuilder().addAllVal(Arrays.asList(1, 2)).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); assertThat(validate.getViolations()).hasSize(1); } @@ -172,34 +176,35 @@ public void strictRepeatedExact() throws Exception { public void strictSFixed64In() throws Exception { SFixed64In invalid = SFixed64In.newBuilder().setVal(5).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); assertThat(validate.getViolations()).hasSize(1); } @Test public void strictFieldExpressionsNested() throws Exception { - FieldExpressionNestedScalar invalid = - FieldExpressionNestedScalar.newBuilder() - .setNested(FieldExpressionScalar.newBuilder().setVal(2)) + FieldExpressions invalid = + FieldExpressions.newBuilder() + .setA(42) + .setC(FieldExpressions.Nested.newBuilder().setA(-3).build()) .build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); - assertThat(validate.getViolations()).hasSize(1); + assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations()).hasSize(4); } @Test public void strictRepeatedExactIgnore() throws Exception { RepeatedExactIgnore invalid = RepeatedExactIgnore.newBuilder().build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations().isEmpty()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); } @Test public void strictInt32In() throws Exception { Int32In invalid = Int32In.newBuilder().setVal(4).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); assertThat(validate.getViolations()).hasSize(1); } @@ -207,7 +212,7 @@ public void strictInt32In() throws Exception { public void strictRepeatedEnumIn() throws Exception { RepeatedEnumIn invalid = RepeatedEnumIn.newBuilder().addVal(AnEnum.AN_ENUM_X).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); assertThat(validate.getViolations()).hasSize(1); } @@ -219,29 +224,30 @@ public void strictRepeatedMin() throws Exception { .addVal(Embed.newBuilder().setVal(-1).build()) .build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); assertThat(validate.getViolations()).hasSize(1); } - @Test - public void testDynRuntimeError() { + @Test(expected = ExecutionException.class) + public void testDynRuntimeError() throws Exception { DynRuntimeError invalid = DynRuntimeError.newBuilder().setA(123).build(); - assertThatThrownBy(() -> validator.validate(invalid)).isInstanceOf(ExecutionException.class); + validator.validate(invalid); } - @Test - public void strictBytesIn() throws ValidationException { - BytesIn invalid = BytesIn.newBuilder().setVal(ByteString.copyFromUtf8("bar")).build(); - ValidationResult validate = validator.validate(invalid); - assertThat(validate.getViolations()).isEmpty(); - } + // Needs : https://github.com/projectnessie/cel-java/pull/419 + // @Test + // public void strictBytesIn() throws ValidationException { + // BytesIn invalid = BytesIn.newBuilder().setVal(ByteString.copyFromUtf8("bar")).build(); + // ValidationResult validate = validator.validate(invalid); + // assertThat(validate.isSuccess()).isTrue(); + // } @Test public void strictRepeatedUnique() throws ValidationException { RepeatedUnique invalid = RepeatedUnique.newBuilder().addAllVal(Arrays.asList("foo", "bar", "foo", "baz")).build(); ValidationResult validate = validator.validate(invalid); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test @@ -258,22 +264,14 @@ public void testRecursiveInvalid() throws ValidationException { MapRecursive.newBuilder().putVal(1, MapRecursive.Msg.newBuilder().build()).build(); ValidationResult validate = validator.validate(test); assertThat(validate.getViolations()).hasSize(1); - assertThat(validate.isSuccess()).isFalse(); + assertThat(validate.isSuccess()).isTrue(); } @Test public void testStringLenEmoji() throws ValidationException { StringLen test = StringLen.newBuilder().setVal("😅😄👾").build(); ValidationResult validate = validator.validate(test); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); - } - - @Test - public void testDurationIn() throws ValidationException { - DurationIn test = DurationIn.newBuilder().setVal(Duration.newBuilder().setSeconds(1)).build(); - ValidationResult validate = validator.validate(test); - assertThat(validate.getViolations()).isEmpty(); - assertThat(validate.isSuccess()).isTrue(); + assertThat(validate.getViolations()).hasSize(0); + assertThat(validate.getViolations().isEmpty()).isTrue(); } } diff --git a/src/test/java/build/buf/protovalidate/WellKnownRegexTest.java b/src/test/java/build/buf/protovalidate/WellKnownRegexTest.java deleted file mode 100644 index e4aafdca5..000000000 --- a/src/test/java/build/buf/protovalidate/WellKnownRegexTest.java +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.noimports.validationtest.HttpHeaderName; -import com.example.noimports.validationtest.HttpHeaderNameLoose; -import com.example.noimports.validationtest.HttpHeaderValue; -import org.junit.jupiter.api.Test; - -/** - * Tests for the {@code well_known_regex} oneof case in {@link StringRulesEvaluator}: HTTP header - * name and value, in both strict and loose modes, plus the empty-header-name special case. - */ -class WellKnownRegexTest { - - private final Validator nativeValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(true).build()) - .build(); - - @Test - void headerName_strict_passesValidName() throws ValidationException { - HttpHeaderName msg = HttpHeaderName.newBuilder().setVal("X-Request-Id").build(); - assertThat(nativeValidator.validate(msg).isSuccess()).isTrue(); - } - - @Test - void headerName_strict_failsInvalidName() throws ValidationException { - HttpHeaderName msg = HttpHeaderName.newBuilder().setVal("not a header").build(); - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation v = result.getViolations().get(0).toProto(); - assertThat(v.getRuleId()).isEqualTo("string.well_known_regex.header_name"); - assertThat(v.getMessage()).isEqualTo("must be a valid HTTP header name"); - } - - @Test - void headerName_emptyValue_firesEmptyVariant() throws ValidationException { - // Empty header name is a separate rule id with its own message. - HttpHeaderName msg = HttpHeaderName.newBuilder().setVal("").build(); - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation v = result.getViolations().get(0).toProto(); - assertThat(v.getRuleId()).isEqualTo("string.well_known_regex.header_name_empty"); - assertThat(v.getMessage()).isEqualTo("value is empty, which is not a valid HTTP header name"); - } - - @Test - void headerName_loose_acceptsValueStrictWouldReject() throws ValidationException { - // Strict regex would reject spaces; loose just forbids null/CR/LF. - HttpHeaderNameLoose msg = - HttpHeaderNameLoose.newBuilder().setVal("any header with spaces").build(); - assertThat(nativeValidator.validate(msg).isSuccess()).isTrue(); - } - - @Test - void headerValue_strict_passesValidValue() throws ValidationException { - HttpHeaderValue msg = HttpHeaderValue.newBuilder().setVal("text/plain").build(); - assertThat(nativeValidator.validate(msg).isSuccess()).isTrue(); - } - - @Test - void headerValue_strict_failsControlChar() throws ValidationException { - // 0x01 is in the forbidden range for strict header values. - HttpHeaderValue msg = HttpHeaderValue.newBuilder().setVal("").build(); - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation v = result.getViolations().get(0).toProto(); - assertThat(v.getRuleId()).isEqualTo("string.well_known_regex.header_value"); - assertThat(v.getMessage()).isEqualTo("must be a valid HTTP header value"); - } - - @Test - void headerValue_emptyValueIsValid() throws ValidationException { - // Header value pattern is '*' (zero-or-more), so empty is allowed under strict mode and - // there is no header_value_empty variant. - HttpHeaderValue msg = HttpHeaderValue.newBuilder().setVal("").build(); - assertThat(nativeValidator.validate(msg).isSuccess()).isTrue(); - } -} diff --git a/src/test/java/build/buf/protovalidate/WrappedValueEvaluatorTest.java b/src/test/java/build/buf/protovalidate/WrappedValueEvaluatorTest.java deleted file mode 100644 index 22a4f1825..000000000 --- a/src/test/java/build/buf/protovalidate/WrappedValueEvaluatorTest.java +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package build.buf.protovalidate; - -import static org.assertj.core.api.Assertions.assertThat; - -import build.buf.protovalidate.exceptions.ValidationException; -import com.example.noimports.validationtest.Int64WrapperConst; -import com.example.noimports.validationtest.StringWrapperLen; -import com.google.protobuf.Int64Value; -import com.google.protobuf.StringValue; -import org.junit.jupiter.api.Test; - -/** - * Targeted tests for {@link WrappedValueEvaluator}. Mirrors the wrapper-unwrap path that the native - * dispatcher takes for {@code google.protobuf.*Value} fields. The conformance suite covers one - * wrapper kind (DoubleValue) via parity tests; these cover the unwrap invariant directly. - */ -class WrappedValueEvaluatorTest { - - private final Validator nativeValidator = - ValidatorFactory.newBuilder() - .withConfig(Config.newBuilder().setEnableNativeRules(true).build()) - .build(); - - @Test - void absentWrapperFieldProducesNoViolation() throws ValidationException { - // Field unset (proto3 message-typed field absent): there is no value to validate, so the - // wrapped scalar evaluator should not be invoked. WrappedValueEvaluator's contract returns - // NO_VIOLATIONS in that case. - Int64WrapperConst msg = Int64WrapperConst.newBuilder().build(); - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.isSuccess()).isTrue(); - } - - @Test - void presentWrapperWithDefaultInnerValueFiresRule() throws ValidationException { - // Wrapper present with default inner (0). Rule is const=5 — 0 != 5 so the rule fires. - // This proves the unwrap reaches the inner field rather than seeing the wrapper Message. - Int64WrapperConst msg = - Int64WrapperConst.newBuilder().setVal(Int64Value.newBuilder().build()).build(); - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("int64.const"); - assertThat(proto.getField().getElements(0).getFieldName()).isEqualTo("val"); - } - - @Test - void presentWrapperWithViolatingValueProducesExpectedShape() throws ValidationException { - StringWrapperLen msg = - StringWrapperLen.newBuilder() - .setVal(StringValue.newBuilder().setValue("ab").build()) - .build(); - ValidationResult result = nativeValidator.validate(msg); - assertThat(result.getViolations()).hasSize(1); - build.buf.validate.Violation proto = result.getViolations().get(0).toProto(); - assertThat(proto.getRuleId()).isEqualTo("string.min_len"); - // Field path points at the wrapper-typed field, not the synthetic inner "value". - assertThat(proto.getField().getElements(0).getFieldName()).isEqualTo("val"); - } - - @Test - void presentWrapperWithPassingValueProducesNoViolation() throws ValidationException { - Int64WrapperConst msg = - Int64WrapperConst.newBuilder().setVal(Int64Value.newBuilder().setValue(5).build()).build(); - assertThat(nativeValidator.validate(msg).isSuccess()).isTrue(); - } -} diff --git a/src/test/resources/proto/buf.gen.cel.testtypes.yaml b/src/test/resources/proto/buf.gen.cel.testtypes.yaml deleted file mode 100644 index 7617ebcfd..000000000 --- a/src/test/resources/proto/buf.gen.cel.testtypes.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: v2 -managed: - enabled: true - override: - - file_option: java_package - value: cel.expr.conformance.proto3 -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/test-sources/bufgen-cel-testtypes diff --git a/src/test/resources/proto/buf.gen.cel.yaml b/src/test/resources/proto/buf.gen.cel.yaml deleted file mode 100644 index 1eada8cb8..000000000 --- a/src/test/resources/proto/buf.gen.cel.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: v2 -managed: - enabled: true -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/test-sources/bufgen-cel diff --git a/src/test/resources/proto/buf.gen.imports.yaml b/src/test/resources/proto/buf.gen.imports.yaml deleted file mode 100644 index f9ad45a4d..000000000 --- a/src/test/resources/proto/buf.gen.imports.yaml +++ /dev/null @@ -1,11 +0,0 @@ -version: v2 -managed: - enabled: true - override: - - file_option: java_package_prefix - value: com.example.imports -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/test-sources/bufgen-imports -inputs: - - directory: src/test/resources/proto diff --git a/src/test/resources/proto/buf.gen.noimports.yaml b/src/test/resources/proto/buf.gen.noimports.yaml deleted file mode 100644 index d8db0d53e..000000000 --- a/src/test/resources/proto/buf.gen.noimports.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: v2 -managed: - enabled: true - disable: - - file_option: java_package - module: buf.build/bufbuild/protovalidate - override: - - file_option: java_package_prefix - value: com.example.noimports -plugins: - - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion - out: build/generated/test-sources/bufgen-noimports -inputs: - - directory: src/main/resources - - directory: src/test/resources/proto diff --git a/src/test/resources/proto/validationtest/custom_rules.proto b/src/test/resources/proto/validationtest/custom_rules.proto deleted file mode 100644 index 56d3da4cc..000000000 --- a/src/test/resources/proto/validationtest/custom_rules.proto +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package validationtest; - -import "buf/validate/validate.proto"; - -message FieldExpressionRepeatedMessage { - repeated Msg val = 1 [(buf.validate.field).cel = { - id: "field_expression.repeated.message" - message: "test message field_expression.repeated.message" - expression: "this.all(e, e.a == 1)" - }]; - message Msg { - int32 a = 1; - } -} - -message FieldExpressionRepeatedMessageItems { - repeated Msg val = 1 [(buf.validate.field).repeated.items.cel = { - id: "field_expression.repeated.message.items" - message: "test message field_expression.repeated.message.items" - expression: "this.a == 1" - }]; - message Msg { - int32 a = 1; - } -} diff --git a/src/test/resources/proto/validationtest/import_test.proto b/src/test/resources/proto/validationtest/import_test.proto deleted file mode 100644 index 4ba07c147..000000000 --- a/src/test/resources/proto/validationtest/import_test.proto +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package validationtest; - -import "buf/validate/validate.proto"; - -message ExampleImportedMessage { - string hex_string = 1 [(buf.validate.field).string.pattern = "^[0-9a-fA-F]+$"]; -} diff --git a/src/test/resources/proto/validationtest/issue427.proto b/src/test/resources/proto/validationtest/issue427.proto deleted file mode 100644 index 821ea6d84..000000000 --- a/src/test/resources/proto/validationtest/issue427.proto +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package validationtest; - -import "buf/validate/validate.proto"; - -message Issue427 { - option (buf.validate.message).oneof = { - fields: ["name", "tags", "mappings"] - required: true - }; - string name = 1; - repeated string tags = 2; - map mappings = 3; -} diff --git a/src/test/resources/proto/validationtest/predefined.proto b/src/test/resources/proto/validationtest/predefined.proto deleted file mode 100644 index 06fdf1334..000000000 --- a/src/test/resources/proto/validationtest/predefined.proto +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto2"; - -package validationtest; - -import "buf/validate/validate.proto"; - -extend buf.validate.StringRules { - optional bool is_ident = 1161 [(buf.validate.predefined).cel = { - id: "string.is_ident" - expression: "(rule && !this.matches('^[a-z0-9]{1,9}$')) ? 'invalid identifier' : ''" - }]; -} - -message ExamplePredefinedFieldRules { - optional string ident_field = 1 [(buf.validate.field).string.(is_ident) = true]; -} diff --git a/src/test/resources/proto/validationtest/required.proto b/src/test/resources/proto/validationtest/required.proto deleted file mode 100644 index eda000c33..000000000 --- a/src/test/resources/proto/validationtest/required.proto +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto2"; - -package validationtest; - -import "buf/validate/validate.proto"; - -message ExampleRequiredFieldRules { - required string regex_string_field = 1 [(buf.validate.field).string.pattern = "^[a-z0-9]{1,9}$"]; - optional string unconstrained = 2; -} diff --git a/src/test/resources/proto/validationtest/validationtest.proto b/src/test/resources/proto/validationtest/validationtest.proto deleted file mode 100644 index a49c8fc68..000000000 --- a/src/test/resources/proto/validationtest/validationtest.proto +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright 2023-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package validationtest; - -import "buf/validate/validate.proto"; -import "google/protobuf/wrappers.proto"; -import "validationtest/import_test.proto"; - -message ExampleFieldRules { - string regex_string_field = 1 [(buf.validate.field).string.pattern = "^[a-z0-9]{1,9}$"]; - string unconstrained = 2; -} - -message ExampleOneofRules { - // contact_info is the user's contact information - oneof contact_info { - // required ensures that exactly one field in oneof is set. Without this - // option, at most one of email and phone_number is set. - option (buf.validate.oneof).required = true; - // email is the user's email - string email = 1; - // phone_number is the user's phone number. - string phone_number = 2; - } - oneof unconstrained { - string field3 = 3; - string field4 = 4; - } -} - -message ExampleMessageRules { - option (buf.validate.message).cel = { - id: "secondary_email_depends_on_primary" - expression: - "has(this.secondary_email) && !has(this.primary_email)" - "? 'cannot set a secondary email without setting a primary one'" - ": ''" - }; - string primary_email = 1; - string secondary_email = 2; -} - -message FieldExpressionMultiple { - string val = 1 [ - (buf.validate.field).string.max_len = 5, - (buf.validate.field).string.pattern = "^[a-z0-9]$" - ]; -} - -message FieldExpressionMapInt32 { - map val = 1 [(buf.validate.field).cel = { - id: "field_expression.map.int32" - message: "all map values must equal 1" - expression: "this.all(k, this[k] == 1)" - }]; -} - -message ExampleImportMessage { - option (buf.validate.message) = { - cel: { - id: "imported_submessage_must_not_be_null" - expression: "this.imported_submessage != null" - } - cel: { - id: "hex_string_must_not_be_empty" - expression: "this.imported_submessage.hex_string != ''" - } - }; - ExampleImportedMessage imported_submessage = 1; -} - -message ExampleImportMessageFieldRule { - ExampleImportMessage message_with_import = 1 [ - (buf.validate.field).cel = { - id: "field_must_not_be_null" - expression: "this.imported_submessage != null" - }, - (buf.validate.field).cel = { - id: "field_string_must_not_be_empty" - expression: "this.imported_submessage.hex_string != ''" - } - ]; -} - -message ExampleImportMessageInMap { - option (buf.validate.message) = { - cel: { - id: "imported_submessage_must_not_be_null" - expression: "this.imported_submessage[0] != null" - } - cel: { - id: "hex_string_must_not_be_empty" - expression: "this.imported_submessage[0].hex_string != ''" - } - }; - map imported_submessage = 1; -} - -message ExampleStringExtensions { - string parent = 1; - string path = 2; - - option (buf.validate.message).cel = { - id: "path_matches_parent" - message: "path must be a child of parent" - expression: - "this.path.lastIndexOf('/') >= 0 && " - "this.path.substring(0, this.path.lastIndexOf('/')) == this.parent" - }; -} - -message ExampleImportMessageInMapFieldRule { - ExampleImportMessageInMap message_with_import = 1 [ - (buf.validate.field).cel = { - id: "field_must_not_be_null" - expression: "this.imported_submessage[0] != null" - }, - (buf.validate.field).cel = { - id: "field_string_must_not_be_empty" - expression: "this.imported_submessage[0].hex_string != ''" - } - ]; -} - -message ExampleBoolConst { - bool flag = 1 [(buf.validate.field).bool.const = true]; -} - -message ExampleInt32Const { - int32 val = 1 [(buf.validate.field).int32.const = 5]; -} - -message ExampleInt32GtLt { - int32 val = 1 [(buf.validate.field).int32 = { - gt: 0 - lt: 10 - }]; -} - -message ExampleUint32Gt { - // Threshold above Integer.MAX_VALUE — verifies unsigned comparison: a uint32 value of - // 3000000000 must satisfy gt=2147483648, even though both render as negative when - // interpreted as signed int. - uint32 val = 1 [(buf.validate.field).uint32.gt = 2147483648]; -} - -message ExampleFloatFinite { - float val = 1 [(buf.validate.field).float.finite = true]; -} - -message ExampleDoubleIn { - double val = 1 [(buf.validate.field).double = { - in: [ - 1.5, - 2.5 - ] - }]; -} - -enum ExampleColor { - EXAMPLE_COLOR_UNSPECIFIED = 0; - EXAMPLE_COLOR_RED = 1; - EXAMPLE_COLOR_GREEN = 2; - EXAMPLE_COLOR_BLUE = 3; -} - -message ExampleEnumConst { - ExampleColor val = 1 [(buf.validate.field).enum.const = 2]; -} - -message ExampleEnumIn { - ExampleColor val = 1 [(buf.validate.field).enum = { - in: [ - 1, - 3 - ] - }]; -} - -message ExampleBytesConst { - bytes val = 1 [(buf.validate.field).bytes.const = "\x00\x99"]; -} - -message ExampleBytesPattern { - bytes val = 1 [(buf.validate.field).bytes.pattern = "^[a-z]+$"]; -} - -message ExampleBytesIPv4 { - bytes val = 1 [(buf.validate.field).bytes.ipv4 = true]; -} - -message ExampleStringConst { - string val = 1 [(buf.validate.field).string.const = "abcd"]; -} - -message ExampleStringEmail { - string val = 1 [(buf.validate.field).string.email = true]; -} - -message ExampleStringMinMaxLen { - string val = 1 [(buf.validate.field).string = { - min_len: 2 - max_len: 5 - }]; -} - -message ExampleStringHostAndPort { - string val = 1 [(buf.validate.field).string.host_and_port = true]; -} - -message ExampleRepeatedMinMax { - repeated int32 val = 1 [(buf.validate.field).repeated = { - min_items: 2 - max_items: 5 - }]; -} - -message ExampleRepeatedUnique { - repeated string val = 1 [(buf.validate.field).repeated.unique = true]; -} - -message ExampleMapMinMax { - map val = 1 [(buf.validate.field).map = { - min_pairs: 1 - max_pairs: 3 - }]; -} - -// --- Float/double parity fixtures --- -// -// See NATIVE_RULES_REVIEW.md sections B1 (fixed) and B2 (reclassified). The tests in -// FloatBugConfirmationTest now lock in parity rather than confirm divergence. - -// Originally documented the floatFormatter sign-strip bug; now used by the regression test -// asserting native and CEL both render -0.0 as "must equal -0". -message ExampleFloatConstNegZero { - float val = 1 [(buf.validate.field).float.const = -0.0]; -} - -message ExampleDoubleConstNegZero { - double val = 1 [(buf.validate.field).double.const = -0.0]; -} - -// Originally claimed a parity divergence on repeated.unique for floats/doubles. Investigation -// showed CustomOverload.uniqueList (the CEL-side impl in protovalidate-java) and -// RepeatedRulesEvaluator.isUnique both use HashSet + Object.equals, so they agree — both -// deviate from CEL's IEEE-754 spec for NaN and +/- 0 in the same direction. The agreement -// tests pin that contract until a coordinated fix lands. -message ExampleFloatRepeatedUnique { - repeated float val = 1 [(buf.validate.field).repeated.unique = true]; -} - -message ExampleDoubleRepeatedUnique { - repeated double val = 1 [(buf.validate.field).repeated.unique = true]; -} - -message FloatDoubleNaNNegZero { - repeated float fvals = 1 [(buf.validate.field).repeated.unique = true]; - repeated double dvals = 2 [(buf.validate.field).repeated.unique = true]; - float fneg_zero = 3 [(buf.validate.field).float.const = -0.0]; - double dneg_zero = 4 [(buf.validate.field).double.const = -0.0]; -} - -// Multi-rule fixtures used to exercise failFast and residual-clearing semantics. -// -// Each field has two scalar rules (or a count + element rule) so a value that violates the -// first rule will also violate the second. With failFast=true the validator should report only -// one violation; with failFast=false (the default), both should be reported. - -// String with min_len=4 and a "must contain digit" pattern. -message StringMultiRule { - string val = 1 [(buf.validate.field).string = { - min_len: 4 - pattern: ".*[0-9].*" - }]; -} - -// Int with gt=10 and in=[5,15]. Default 0 violates both. -message Int32MultiRule { - int32 val = 1 [(buf.validate.field).int32 = { - gt: 10 - in: [ - 5, - 15 - ] - }]; -} - -// Bytes with min_len=4 and ipv4=true. Empty value violates both. -message BytesMultiRule { - bytes val = 1 [(buf.validate.field).bytes = { - min_len: 4 - ipv4: true - }]; -} - -// not_in fixtures, one per kind, used by NotInRulesTest. -message Int32NotIn { - int32 val = 1 [(buf.validate.field).int32 = { - not_in: [ - 1, - 2, - 3 - ] - }]; -} - -message Uint32NotIn { - uint32 val = 1 [(buf.validate.field).uint32 = { - not_in: [ - 1, - 2 - ] - }]; -} - -message StringNotIn { - string val = 1 [(buf.validate.field).string = { - not_in: [ - "foo", - "bar" - ] - }]; -} - -message BytesNotIn { - bytes val = 1 [(buf.validate.field).bytes = { - not_in: [ - "AA", - "BB" - ] - }]; -} - -message EnumNotIn { - ExampleColor val = 1 [(buf.validate.field).enum = { - not_in: [ - 1, - 2 - ] - }]; -} - -// well_known_regex fixtures. -message HttpHeaderName { - string val = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_NAME]; -} - -message HttpHeaderNameLoose { - string val = 1 [(buf.validate.field).string = { - well_known_regex: KNOWN_REGEX_HTTP_HEADER_NAME - strict: false - }]; -} - -message HttpHeaderValue { - string val = 1 [(buf.validate.field).string.well_known_regex = KNOWN_REGEX_HTTP_HEADER_VALUE]; -} - -// google.protobuf.*Value wrapper fixtures for WrappedValueEvaluator coverage. -message Int64WrapperConst { - google.protobuf.Int64Value val = 1 [(buf.validate.field).int64.const = 5]; -} - -message StringWrapperLen { - google.protobuf.StringValue val = 1 [(buf.validate.field).string.min_len = 3]; -} diff --git a/src/test/resources/testdata/string_ext_supplemental.textproto b/src/test/resources/testdata/string_ext_supplemental.textproto deleted file mode 100644 index dde7b83b9..000000000 --- a/src/test/resources/testdata/string_ext_supplemental.textproto +++ /dev/null @@ -1,26 +0,0 @@ -# proto-file: ../../../proto/cel/expr/conformance/test/simple.proto -# proto-message: cel.expr.conformance.test.SimpleTestFile - -# Ideally these tests should be in the cel-spec conformance test suite. -# Until they are added, we can use this to test for additional functionality -# listed in the spec. - -name: "string_ext_supplemental" -description: "Supplemental tests for the strings extension library." -section: { - name: "format" - test: { - name: "bytes support for string with invalid utf-8 encoding" - expr: '"%s".format([b"\\xF0abc\\x8C\\xF0xyz"])' - value: { - string_value: '\ufffdabc\ufffdxyz', - } - } - test: { - name: "bytes support for string with only invalid utf-8 sequences" - expr: '"%s".format([b"\\xF0\\x8C\\xF0"])' - value: { - string_value: '\ufffd', - } - } -} diff --git a/src/test/resources/testdata/string_ext_v0.24.0.textproto b/src/test/resources/testdata/string_ext_v0.24.0.textproto deleted file mode 100644 index d2455583f..000000000 --- a/src/test/resources/testdata/string_ext_v0.24.0.textproto +++ /dev/null @@ -1,1417 +0,0 @@ -# proto-file: ../../../proto/cel/expr/conformance/test/simple.proto -# proto-message: cel.expr.conformance.test.SimpleTestFile - -name: "string_ext" -description: "Tests for the strings extension library." -section: { - name: "char_at" - test: { - name: "middle_index" - expr: "'tacocat'.charAt(3)" - value: { - string_value: "o" - } - } - test: { - name: "end_index" - expr: "'tacocat'.charAt(7)" - value: { - string_value: "" - } - } - test: { - name: "multiple" - expr: "'©αT'.charAt(0) == '©' && '©αT'.charAt(1) == 'α' && '©αT'.charAt(2) == 'T'" - } -} -section: { - name: "index_of" - test: { - name: "empty_index" - expr: "'tacocat'.indexOf('')" - value: { - int64_value: 0 - } - } - test: { - name: "string_index" - expr: "'tacocat'.indexOf('ac')" - value: { - int64_value: 1 - } - } - test: { - name: "nomatch" - expr: "'tacocat'.indexOf('none') == -1" - } - test: { - name: "empty_index" - expr: "'tacocat'.indexOf('', 3) == 3" - } - test: { - name: "char_index" - expr: "'tacocat'.indexOf('a', 3) == 5" - } - test: { - name: "string_index" - expr: "'tacocat'.indexOf('at', 3) == 5" - } - test: { - name: "unicode_char" - expr: "'ta©o©αT'.indexOf('©') == 2" - } - test: { - name: "unicode_char_index" - expr: "'ta©o©αT'.indexOf('©', 3) == 4" - } - test: { - name: "unicode_string_index" - expr: "'ta©o©αT'.indexOf('©αT', 3) == 4" - } - test: { - name: "unicode_string_nomatch_index" - expr: "'ta©o©αT'.indexOf('©α', 5) == -1" - } - test: { - name: "char_index" - expr: "'ijk'.indexOf('k') == 2" - } - test: { - name: "string_with_space_fullmatch" - expr: "'hello wello'.indexOf('hello wello') == 0" - } - test: { - name: "string_with_space_index" - expr: "'hello wello'.indexOf('ello', 6) == 7" - } - test: { - name: "string_nomatch_index" - expr: "'hello wello'.indexOf('elbo room!!') == -1" - } -} -section: { - name: "last_index_of" - test: { - name: "empty" - expr: "'tacocat'.lastIndexOf('') == 7" - } - test: { - name: "string" - expr: "'tacocat'.lastIndexOf('at') == 5" - } - test: { - name: "string_nomatch" - expr: "'tacocat'.lastIndexOf('none') == -1" - } - test: { - name: "empty_index" - expr: "'tacocat'.lastIndexOf('', 3) == 3" - } - test: { - name: "char_index" - expr: "'tacocat'.lastIndexOf('a', 3) == 1" - } - test: { - name: "unicode_char" - expr: "'ta©o©αT'.lastIndexOf('©') == 4" - } - test: { - name: "unicode_char_index" - expr: "'ta©o©αT'.lastIndexOf('©', 3) == 2" - } - test: { - name: "unicode_string_index" - expr: "'ta©o©αT'.lastIndexOf('©α', 4) == 4" - } - test: { - name: "string_with_space_string_index" - expr: "'hello wello'.lastIndexOf('ello', 6) == 1" - } - test: { - name: "string_with_space_string_nomatch" - expr: "'hello wello'.lastIndexOf('low') == -1" - } - test: { - name: "string_with_space_string_with_space_nomatch" - expr: "'hello wello'.lastIndexOf('elbo room!!') == -1" - } - test: { - name: "string_with_space_fullmatch" - expr: "'hello wello'.lastIndexOf('hello wello') == 0" - } - test: { - name: "repeated_string" - expr: "'bananananana'.lastIndexOf('nana', 7) == 6" - } -} -section: { - name: "ascii_casing" - test: { - name: "lowerascii" - expr: "'TacoCat'.lowerAscii() == 'tacocat'" - } - test: { - name: "lowerascii_unicode" - expr: "'TacoCÆt'.lowerAscii() == 'tacocÆt'" - } - test: { - name: "lowerascii_unicode_with_space" - expr: "'TacoCÆt Xii'.lowerAscii() == 'tacocÆt xii'" - } - test: { - name: "upperascii" - expr: "'tacoCat'.upperAscii() == 'TACOCAT'" - } - test: { - name: "upperascii_unicode" - expr: "'tacoCαt'.upperAscii() == 'TACOCαT'" - } - test: { - name: "upperascii_unicode_with_space" - expr: "'TacoCÆt Xii'.upperAscii() == 'TACOCÆT XII'" - } -} -section: { - name: "replace" - test: { - name: "no_placeholder" - expr: "'12 days 12 hours'.replace('{0}', '2') == '12 days 12 hours'" - } - test: { - name: "basic" - expr: "'{0} days {0} hours'.replace('{0}', '2') == '2 days 2 hours'" - } - test: { - name: "chained" - expr: "'{0} days {0} hours'.replace('{0}', '2', 1).replace('{0}', '23') == '2 days 23 hours'" - } - test: { - name: "unicode" - expr: "'1 ©αT taco'.replace('αT', 'o©α') == '1 ©o©α taco'" - } -} -section: { - name: "split" - test: { - name: "empty" - expr: "'hello world'.split(' ') == ['hello', 'world']" - } - test: { - name: "zero_limit" - expr: "'hello world events!'.split(' ', 0) == []" - } - test: { - name: "one_limit" - expr: "'hello world events!'.split(' ', 1) == ['hello world events!']" - } - test: { - name: "unicode_negative_limit" - expr: "'o©o©o©o'.split('©', -1) == ['o', 'o', 'o', 'o']" - } -} -section: { - name: "substring" - test: { - name: "start" - expr: "'tacocat'.substring(4) == 'cat'" - } - test: { - name: "start_with_max_length" - expr: "'tacocat'.substring(7) == ''" - } - test: { - name: "start_and_end" - expr: "'tacocat'.substring(0, 4) == 'taco'" - } - test: { - name: "start_and_end_equal_value" - expr: "'tacocat'.substring(4, 4) == ''" - } - test: { - name: "unicode_start_and_end" - expr: "'ta©o©αT'.substring(2, 6) == '©o©α'" - } - test: { - name: "unicode_start_and_end_equal_value" - expr: "'ta©o©αT'.substring(7, 7) == ''" - } -} -section: { - name: "trim" - test: { - name: "blank_spaces_escaped_chars" - expr: "' \\f\\n\\r\\t\\vtext '.trim() == 'text'" - } - test: { - name: "unicode_space_chars_1" - expr: "'\\u0085\\u00a0\\u1680text'.trim() == 'text'" - } - test: { - name: "unicode_space_chars_2" - expr: "'text\\u2000\\u2001\\u2002\\u2003\\u2004\\u2004\\u2006\\u2007\\u2008\\u2009'.trim() == 'text'" - } - test: { - name: "unicode_space_chars_3" - expr: "'\\u200atext\\u2028\\u2029\\u202F\\u205F\\u3000'.trim() == 'text'" - } - test: { - name: "unicode_no_trim" - expr: "'\\u180etext\\u200b\\u200c\\u200d\\u2060\\ufeff'.trim() == '\\u180etext\\u200b\\u200c\\u200d\\u2060\\ufeff'" - } -} -section: { - name: "join" - test: { - name: "empty_separator" - expr: "['x', 'y'].join() == 'xy'" - } - test: { - name: "dash_separator" - expr: "['x', 'y'].join('-') == 'x-y'" - } - test: { - name: "empty_string_empty_separator" - expr: "[].join() == ''" - } - test: { - name: "empty_string_dash_separator" - expr: "[].join('-') == ''" - } -} -section: { - name: "quote" - test: { - name: "multiline" - expr: "strings.quote(\"first\\nsecond\") == \"\\\"first\\\\nsecond\\\"\"" - } - test: { - name: "escaped" - expr: "strings.quote(\"bell\\a\") == \"\\\"bell\\\\a\\\"\"" - } - test: { - name: "backspace" - expr: "strings.quote(\"\\bbackspace\") == \"\\\"\\\\bbackspace\\\"\"" - } - test: { - name: "form_feed" - expr: "strings.quote(\"\\fform feed\") == \"\\\"\\\\fform feed\\\"\"" - } - test: { - name: "carriage_return" - expr: "strings.quote(\"carriage \\r return\") == \"\\\"carriage \\\\r return\\\"\"" - } - test: { - name: "horizontal_tab" - expr: "strings.quote(\"horizontal tab\\t\") == \"\\\"horizontal tab\\\\t\\\"\"" - } - test: { - name: "vertical_tab" - expr: "strings.quote(\"vertical \\v tab\") == \"\\\"vertical \\\\v tab\\\"\"" - } - test: { - name: "double_slash" - expr: "strings.quote(\"double \\\\\\\\ slash\") == \"\\\"double \\\\\\\\\\\\\\\\ slash\\\"\"" - } - test: { - name: "two_escape_sequences" - expr: "strings.quote(\"two escape sequences \\\\a\\\\n\") == \"\\\"two escape sequences \\\\\\\\a\\\\\\\\n\\\"\"" - } - test: { - name: "verbatim" - expr: "strings.quote(\"verbatim\") == \"\\\"verbatim\\\"\"" - } - test: { - name: "ends_with" - expr: "strings.quote(\"ends with \\\\\") == \"\\\"ends with \\\\\\\\\\\"\"" - } - test: { - name: "starts_with" - expr: "strings.quote(\"\\\\ starts with\") == \"\\\"\\\\\\\\ starts with\\\"\"" - } - test: { - name: "printable_unicode" - expr: "strings.quote(\"printable unicode😀\") == \"\\\"printable unicode😀\\\"\"" - } - test: { - name: "mid_string_quote" - expr: "strings.quote(\"mid string \\\" quote\") == \"\\\"mid string \\\\\\\" quote\\\"\"" - } - test: { - name: "single_quote_with_double_quote" - expr: "strings.quote('single-quote with \"double quote\"') == \"\\\"single-quote with \\\\\\\"double quote\\\\\\\"\\\"\"" - } - test: { - name: "size_unicode_char" - expr: "strings.quote(\"size('ÿ')\") == \"\\\"size('ÿ')\\\"\"" - } - test: { - name: "size_unicode_string" - expr: "strings.quote(\"size('πέντε')\") == \"\\\"size('πέντε')\\\"\"" - } - test: { - name: "unicode" - expr: "strings.quote(\"завтра\") == \"\\\"завтра\\\"\"" - } - test: { - name: "unicode_code_points" - expr: "strings.quote(\"\\U0001F431\\U0001F600\\U0001F61B\")" - value: { - string_value: "\"🐱😀😛\"" - } - } - test: { - name: "unicode_2" - expr: "strings.quote(\"ta©o©αT\") == \"\\\"ta©o©αT\\\"\"" - } - test: { - name: "empty_quote" - expr: "strings.quote(\"\")" - value: { - string_value: "\"\"" - } - } -} -section: { - name: "format" - test: { - name: "no-op" - expr: '"no substitution".format([])' - value: { - string_value: 'no substitution', - } - } - test: { - name: "mid-string substitution" - expr: '"str is %s and some more".format(["filler"])' - value: { - string_value: 'str is filler and some more', - } - } - test: { - name: "percent escaping" - expr: '"%% and also %%".format([])' - value: { - string_value: '% and also %', - } - } - test: { - name: "substitution inside escaped percent signs" - expr: '"%%%s%%".format(["text"])' - value: { - string_value: '%text%', - } - } - test: { - name: "substitution with one escaped percent sign on the right" - expr: '"%s%%".format(["percent on the right"])' - value: { - string_value: 'percent on the right%', - } - } - test: { - name: "substitution with one escaped percent sign on the left" - expr: '"%%%s".format(["percent on the left"])' - value: { - string_value: '%percent on the left', - } - } - test: { - name: "multiple substitutions" - expr: '"%d %d %d, %s %s %s, %d %d %d, %s %s %s".format([1, 2, 3, "A", "B", "C", 4, 5, 6, "D", "E", "F"])' - value: { - string_value: '1 2 3, A B C, 4 5 6, D E F', - } - } - test: { - name: "percent sign escape sequence support" - expr: '"%%escaped %s%%".format(["percent"])' - value: { - string_value: '%escaped percent%', - } - } - test: { - name: "fixed point formatting clause" - expr: '"%.3f".format([1.2345])' - value: { - string_value: '1.234', - } - } - test: { - name: "binary formatting clause" - expr: '"this is 5 in binary: %b".format([5])' - value: { - string_value: 'this is 5 in binary: 101', - } - } - test: { - name: "uint support for binary formatting" - expr: '"unsigned 64 in binary: %b".format([uint(64)])' - value: { - string_value: 'unsigned 64 in binary: 1000000', - } - } - test: { - name: "bool support for binary formatting" - expr: '"bit set from bool: %b".format([true])' - value: { - string_value: 'bit set from bool: 1', - } - } - test: { - name: "octal formatting clause" - expr: '"%o".format([11])' - value: { - string_value: '13', - } - } - test: { - name: "uint support for octal formatting clause" - expr: '"this is an unsigned octal: %o".format([uint(65535)])' - value: { - string_value: 'this is an unsigned octal: 177777', - } - } - test: { - name: "lowercase hexadecimal formatting clause" - expr: '"%x is 20 in hexadecimal".format([30])' - value: { - string_value: '1e is 20 in hexadecimal', - } - } - test: { - name: "uppercase hexadecimal formatting clause" - expr: '"%X is 20 in hexadecimal".format([30])' - value: { - string_value: '1E is 20 in hexadecimal', - } - } - test: { - name: "unsigned support for hexadecimal formatting clause" - expr: '"%X is 6000 in hexadecimal".format([uint(6000)])' - value: { - string_value: '1770 is 6000 in hexadecimal', - } - } - test: { - name: "string support with hexadecimal formatting clause" - expr: '"%x".format(["Hello world!"])' - value: { - string_value: '48656c6c6f20776f726c6421', - } - } - test: { - name: "string support with uppercase hexadecimal formatting clause" - expr: '"%X".format(["Hello world!"])' - value: { - string_value: '48656C6C6F20776F726C6421', - } - } - test: { - name: "byte support with hexadecimal formatting clause" - expr: '"%x".format([b"byte string"])' - value: { - string_value: '6279746520737472696e67', - } - } - test: { - name: "byte support with uppercase hexadecimal formatting clause" - expr: '"%X".format([b"byte string"])' - value: { - string_value: '6279746520737472696E67', - } - } - test: { - name: "scientific notation formatting clause" - expr: '"%.6e".format([1052.032911275])' - value: { - string_value: '1.052033e+03', - } - } - test: { - name: "default precision for fixed-point clause" - expr: '"%f".format([2.71828])' - value: { - string_value: '2.718280', - } - } - test: { - name: "default precision for scientific notation" - expr: '"%e".format([2.71828])' - value: { - string_value: '2.718280e+00', - } - } - test: { - name: "NaN support for scientific notation" - expr: '"%e".format([double("NaN")])' - value: { - string_value: 'NaN', - } - } - test: { - name: "positive infinity support for scientific notation" - expr: '"%e".format([double("Infinity")])' - value: { - string_value: 'Infinity', - } - } - test: { - name: "negative infinity support for scientific notation" - expr: '"%e".format([double("-Infinity")])' - value: { - string_value: '-Infinity', - } - } - test: { - name: "NaN support for decimal" - expr: '"%d".format([double("NaN")])' - value: { - string_value: 'NaN', - } - } - test: { - name: "positive infinity support for decimal" - expr: '"%d".format([double("Infinity")])' - value: { - string_value: 'Infinity', - } - } - test: { - name: "negative infinity support for decimal" - expr: '"%d".format([double("-Infinity")])' - value: { - string_value: '-Infinity', - } - } - test: { - name: "NaN support for fixed-point" - expr: '"%f".format([double("NaN")])' - value: { - string_value: 'NaN', - } - } - test: { - name: "positive infinity support for fixed-point" - expr: '"%f".format([double("Infinity")])' - value: { - string_value: 'Infinity', - } - } - test: { - name: "negative infinity support for fixed-point" - expr: '"%f".format([double("-Infinity")])' - value: { - string_value: '-Infinity', - } - } - test: { - name: "uint support for decimal clause" - expr: '"%d".format([uint(64)])' - value: { - string_value: '64', - } - } - test: { - name: "null support for string" - expr: '"%s".format([null])' - value: { - string_value: 'null', - } - } - test: { - name: "int support for string" - expr: '"%s".format([999999999999])' - value: { - string_value: '999999999999', - } - } - test: { - name: "bytes support for string" - expr: '"%s".format([b"xyz"])' - value: { - string_value: 'xyz', - } - } - test: { - name: "type() support for string" - expr: '"%s".format([type("test string")])' - value: { - string_value: 'string', - } - } - test: { - name: "timestamp support for string" - expr: '"%s".format([timestamp("2023-02-03T23:31:20+00:00")])' - value: { - string_value: '2023-02-03T23:31:20Z', - } - } - test: { - name: "duration support for string" - expr: '"%s".format([duration("1h45m47s")])' - value: { - string_value: '6347s', - } - } - test: { - name: "list support for string" - expr: '"%s".format([["abc", 3.14, null, [9, 8, 7, 6], timestamp("2023-02-03T23:31:20Z")]])' - value: { - string_value: '[abc, 3.14, null, [9, 8, 7, 6], 2023-02-03T23:31:20Z]', - } - } - test: { - name: "map support for string" - expr: '"%s".format([{"key1": b"xyz", "key5": null, "key2": duration("2h"), "key4": true, "key3": 2.71828}])' - value: { - string_value: '{key1: xyz, key2: 7200s, key3: 2.71828, key4: true, key5: null}', - } - } - test: { - name: "map support (all key types)" - expr: '"%s".format([{1: "value1", uint(2): "value2", true: double("NaN")}])' - value: { - string_value: '{1: value1, 2: value2, true: NaN}', - } - } - test: { - name: "boolean support for %s" - expr: '"%s, %s".format([true, false])' - value: { - string_value: 'true, false', - } - } - test: { - name: "dyntype support for string formatting clause" - expr: '"%s".format([dyn("a string")])' - value: { - string_value: 'a string', - } - } - test: { - name: "dyntype support for numbers with string formatting clause" - expr: '"%s, %s".format([dyn(32), dyn(56.8)])' - value: { - string_value: '32, 56.8', - } - } - test: { - name: "dyntype support for integer formatting clause" - expr: '"%d".format([dyn(128)])' - value: { - string_value: '128', - } - } - test: { - name: "dyntype support for integer formatting clause (unsigned)" - expr: '"%d".format([dyn(256u)])' - value: { - string_value: '256', - } - } - test: { - name: "dyntype support for hex formatting clause" - expr: '"%x".format([dyn(22)])' - value: { - string_value: '16', - } - } - test: { - name: "dyntype support for hex formatting clause (uppercase)" - expr: '"%X".format([dyn(26)])' - value: { - string_value: '1A', - } - } - test: { - name: "dyntype support for unsigned hex formatting clause" - expr: '"%x".format([dyn(500u)])' - value: { - string_value: '1f4', - } - } - test: { - name: "dyntype support for fixed-point formatting clause" - expr: '"%.3f".format([dyn(4.5)])' - value: { - string_value: '4.500', - } - } - test: { - name: "dyntype support for scientific notation" - expr: '"%e".format([dyn(2.71828)])' - value: { - string_value: '2.718280e+00', - } - } - test: { - name: "dyntype NaN/infinity support" - expr: '"%s".format([[double("NaN"), double("Infinity"), double("-Infinity")]])' - value: { - string_value: '[NaN, Infinity, -Infinity]', - } - } - test: { - name: "dyntype support for timestamp" - expr: '"%s".format([dyn(timestamp("2009-11-10T23:00:00Z"))])' - value: { - string_value: '2009-11-10T23:00:00Z', - } - } - test: { - name: "dyntype support for duration" - expr: '"%s".format([dyn(duration("8747s"))])' - value: { - string_value: '8747s', - } - } - test: { - name: "dyntype support for lists" - expr: '"%s".format([dyn([6, 4.2, "a string"])])' - value: { - string_value: '[6, 4.2, a string]', - } - } - test: { - name: "dyntype support for maps" - expr: '"%s".format([{"strKey":"x", 6:duration("422s"), true:42}])' - value: { - string_value: '{6: 422s, strKey: x, true: 42}', - } - } - test: { - name: "string substitution in a string variable" - expr: 'str_var.format(["filler"])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%s" } } - } - value: { - string_value: 'filler', - } - } - test: { - name: "multiple substitutions in a string variable" - expr: 'str_var.format([1, 2, 3, "A", "B", "C", 4, 5, 6, "D", "E", "F"])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%d %d %d, %s %s %s, %d %d %d, %s %s %s" } } - } - value: { - string_value: '1 2 3, A B C, 4 5 6, D E F', - } - } - test: { - name: "substitution inside escaped percent signs in a string variable" - expr: 'str_var.format(["text"])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%%%s%%" } } - } - value: { - string_value: '%text%', - } - } - test: { - name: "fixed point formatting clause in a string variable" - expr: 'str_var.format([1.2345])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%.3f" } } - } - value: { - string_value: '1.234', - } - } - test: { - name: "binary formatting clause in a string variable" - expr: 'str_var.format([5])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%b" } } - } - value: { - string_value: '101', - } - } - test: { - name: "scientific notation formatting clause in a string variable" - expr: 'str_var.format([1052.032911275])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%.6e" } } - } - value: { - string_value: '1.052033e+03', - } - } - test: { - name: "default precision for fixed-point clause in a string variable" - expr: 'str_var.format([2.71828])' - type_env: { - name: "str_var", - ident: { type: { primitive: STRING } } - } - bindings: { - key: "str_var" - value: { value: { string_value: "%f" } } - } - value: { - string_value: '2.718280', - } - } -} -section: { - name: "format_errors" - test: { - name: "unrecognized formatting clause" - expr: '"%a".format([1])' - disable_check: true - eval_error: { - errors: { - message: 'could not parse formatting clause: unrecognized formatting clause "a"' - } - } - } - test: { - name: "out of bounds arg index" - expr: '"%d %d %d".format([0, 1])' - disable_check: true - eval_error: { - errors: { - message: 'index 2 out of range' - } - } - } - test: { - name: "string substitution is not allowed with binary clause" - expr: '"string is %b".format(["abc"])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: only integers and bools can be formatted as binary, was given string' - } - } - } - test: { - name: "duration substitution not allowed with decimal clause" - expr: '"%d".format([duration("30m2s")])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: decimal clause can only be used on integers, was given google.protobuf.Duration' - } - } - } - test: { - name: "string substitution not allowed with octal clause" - expr: '"octal: %o".format(["a string"])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: octal clause can only be used on integers, was given string' - } - } - } - test: { - name: "double substitution not allowed with hex clause" - expr: '"double is %x".format([0.5])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given double' - } - } - } - test: { - name: "uppercase not allowed for scientific clause" - expr: '"double is %E".format([0.5])' - disable_check: true - eval_error: { - errors: { - message: 'could not parse formatting clause: unrecognized formatting clause "E"' - } - } - } - test: { - name: "object not allowed" - expr: '"object is %s".format([cel.expr.conformance.proto3.TestAllTypes{}])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given cel.expr.conformance.proto3.TestAllTypes' - } - } - } - test: { - name: "object inside list" - expr: '"%s".format([[1, 2, cel.expr.conformance.proto3.TestAllTypes{}]])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given cel.expr.conformance.proto3.TestAllTypes' - } - } - } - test: { - name: "object inside map" - expr: '"%s".format([{1: "a", 2: cel.expr.conformance.proto3.TestAllTypes{}}])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: string clause can only be used on strings, bools, bytes, ints, doubles, maps, lists, types, durations, and timestamps, was given cel.expr.conformance.proto3.TestAllTypes' - } - } - } - test: { - name: "null not allowed for %d" - expr: '"null: %d".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: decimal clause can only be used on integers, was given null_type' - } - } - } - test: { - name: "null not allowed for %e" - expr: '"null: %e".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: scientific clause can only be used on doubles, was given null_type' - } - } - } - test: { - name: "null not allowed for %f" - expr: '"null: %f".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: fixed-point clause can only be used on doubles, was given null_type' - } - } - } - test: { - name: "null not allowed for %x" - expr: '"null: %x".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given null_type' - } - } - } - test: { - name: "null not allowed for %X" - expr: '"null: %X".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: only integers, byte buffers, and strings can be formatted as hex, was given null_type' - } - } - } - test: { - name: "null not allowed for %b" - expr: '"null: %b".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: only integers and bools can be formatted as binary, was given null_type' - } - } - } - test: { - name: "null not allowed for %o" - expr: '"null: %o".format([null])' - disable_check: true - eval_error: { - errors: { - message: 'error during formatting: octal clause can only be used on integers, was given null_type' - } - } - } -} -section: { - name: "value_errors" - test: { - name: "charat_out_of_range" - expr: "'tacocat'.charAt(30) == ''" - eval_error: { - errors: { - message: "index out of range: 30" - } - } - } - test: { - name: "indexof_out_of_range" - expr: "'tacocat'.indexOf('a', 30) == -1" - eval_error: { - errors: { - message: "index out of range: 30" - } - } - } - test: { - name: "lastindexof_negative_index" - expr: "'tacocat'.lastIndexOf('a', -1) == -1" - eval_error: { - errors: { - message: "index out of range: -1" - } - } - } - test: { - name: "lastindexof_out_of_range" - expr: "'tacocat'.lastIndexOf('a', 30) == -1" - eval_error: { - errors: { - message: "index out of range: 30" - } - } - } - test: { - name: "substring_out_of_range" - expr: "'tacocat'.substring(40) == 'cat'" - eval_error: { - errors: { - message: "index out of range: 40" - } - } - } - test: { - name: "substring_negative_index" - expr: "'tacocat'.substring(-1) == 'cat'" - eval_error: { - errors: { - message: "index out of range: -1" - } - } - } - test: { - name: "substring_end_index_out_of_range" - expr: "'tacocat'.substring(1, 50) == 'cat'" - eval_error: { - errors: { - message: "index out of range: 50" - } - } - } - test: { - name: "substring_begin_index_out_of_range" - expr: "'tacocat'.substring(49, 50) == 'cat'" - eval_error: { - errors: { - message: "index out of range: 49" - } - } - } - test: { - name: "substring_end_index_greater_than_begin_index" - expr: "'tacocat'.substring(4, 3) == ''" - eval_error: { - errors: { - message: "invalid substring range. start: 4, end: 3" - } - } - } -} -section: { - name: "type_errors" - test: { - name: "charat_invalid_type" - expr: "42.charAt(2) == ''" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "charat_invalid_argument" - expr: "'hello'.charAt(true) == ''" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "indexof_unary_invalid_type" - expr: "24.indexOf('2') == 0" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "indexof_unary_invalid_argument" - expr: "'hello'.indexOf(true) == 1" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "indexof_binary_invalid_argument" - expr: "42.indexOf('4', 0) == 0" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "indexof_binary_invalid_argument_2" - expr: "'42'.indexOf(4, 0) == 0" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "indexof_binary_both_invalid_arguments" - expr: "'42'.indexOf('4', '0') == 0" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "indexof_ternary_invalid_arguments" - expr: "'42'.indexOf('4', 0, 1) == 0" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_invalid_type" - expr: "42.split('2') == ['4']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_invalid_type" - expr: "42.replace(2, 1) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_binary_invalid_argument" - expr: "'42'.replace(2, 1) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_binary_invalid_argument_2" - expr: "'42'.replace('2', 1) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_ternary_invalid_argument" - expr: "42.replace('2', '1', 1) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_ternary_invalid_argument_2" - expr: "'42'.replace(2, '1', 1) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_ternary_invalid_argument_3" - expr: "'42'.replace('2', 1, 1) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_ternary_invalid_argument_4" - expr: "'42'.replace('2', '1', '1') == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "replace_quaternary_invalid_argument" - expr: "'42'.replace('2', '1', 1, false) == '41'" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_invalid_type_empty_arg" - expr: "42.split('') == ['4', '2']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_invalid_argument" - expr: "'42'.split(2) == ['4']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_binary_invalid_type" - expr: "42.split('2', '1') == ['4']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_binary_invalid_argument" - expr: "'42'.split(2, 1) == ['4']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_binary_invalid_argument_2" - expr: "'42'.split('2', '1') == ['4']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "split_ternary_invalid_argument" - expr: "'42'.split('2', 1, 1) == ['4']" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "substring_ternary_invalid_argument" - expr: "'hello'.substring(1, 2, 3) == ''" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "substring_binary_invalid_type" - expr: "30.substring(true, 3) == ''" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "substring_binary_invalid_argument" - expr: "'tacocat'.substring(true, 3) == ''" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } - test: { - name: "substring_binary_invalid_argument_2" - expr: "'tacocat'.substring(0, false) == ''" - disable_check: true - eval_error: { - errors: { - message: "no such overload" - } - } - } -}