├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── buf-logo.svg ├── dependabot.yml └── workflows │ ├── add-to-project.yaml │ ├── ci.yaml │ ├── conformance.yaml │ ├── emergency-review-bypass.yaml │ ├── notify-approval-bypass.yaml │ ├── pr-hygiene.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── buf.gen.yaml ├── buf.yaml ├── conformance ├── buf.gen.yaml ├── build.gradle.kts ├── expected-failures.yaml └── src │ ├── main │ └── java │ │ └── build │ │ ├── .DS_Store │ │ └── buf │ │ └── protovalidate │ │ └── conformance │ │ ├── FileDescriptorUtil.java │ │ └── Main.java │ └── test │ └── java │ └── build │ └── buf │ └── protovalidate │ └── ValidatorTest.java ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── java │ └── build │ │ └── buf │ │ └── protovalidate │ │ ├── AnyEvaluator.java │ │ ├── AstExpression.java │ │ ├── CelPrograms.java │ │ ├── CompiledProgram.java │ │ ├── Config.java │ │ ├── CustomDeclarations.java │ │ ├── CustomOverload.java │ │ ├── DescriptorMappings.java │ │ ├── EmbeddedMessageEvaluator.java │ │ ├── EnumEvaluator.java │ │ ├── Evaluator.java │ │ ├── EvaluatorBuilder.java │ │ ├── Expression.java │ │ ├── FieldEvaluator.java │ │ ├── FieldPathUtils.java │ │ ├── Format.java │ │ ├── Ipv4.java │ │ ├── Ipv6.java │ │ ├── ListElementValue.java │ │ ├── ListEvaluator.java │ │ ├── MapEvaluator.java │ │ ├── MessageEvaluator.java │ │ ├── MessageOneofEvaluator.java │ │ ├── MessageValue.java │ │ ├── NowVariable.java │ │ ├── ObjectValue.java │ │ ├── OneofEvaluator.java │ │ ├── ProtoAdapter.java │ │ ├── RuleCache.java │ │ ├── RuleResolver.java │ │ ├── RuleViolation.java │ │ ├── RuleViolationHelper.java │ │ ├── UnknownDescriptorEvaluator.java │ │ ├── Uri.java │ │ ├── ValidateLibrary.java │ │ ├── ValidationResult.java │ │ ├── Validator.java │ │ ├── ValidatorFactory.java │ │ ├── ValidatorImpl.java │ │ ├── Value.java │ │ ├── ValueEvaluator.java │ │ ├── Variable.java │ │ ├── Violation.java │ │ └── exceptions │ │ ├── CompilationException.java │ │ ├── ExecutionException.java │ │ └── ValidationException.java └── resources │ └── buf │ └── validate │ └── validate.proto └── test ├── java └── build │ └── buf │ └── protovalidate │ ├── CustomOverloadTest.java │ ├── FormatTest.java │ ├── ValidationResultTest.java │ ├── ValidatorCelExpressionTest.java │ ├── ValidatorConstructionTest.java │ ├── ValidatorDifferentJavaPackagesTest.java │ ├── ValidatorDynamicMessageTest.java │ └── ValidatorImportTest.java └── resources ├── proto ├── buf.gen.cel.testtypes.yaml ├── buf.gen.cel.yaml ├── buf.gen.imports.yaml ├── buf.gen.noimports.yaml └── validationtest │ ├── custom_rules.proto │ ├── import_test.proto │ ├── predefined.proto │ ├── required.proto │ └── validationtest.proto └── testdata ├── string_ext_supplemental.textproto └── string_ext_v0.24.0.textproto /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | 4 | [*.java] 5 | ij_java_class_count_to_use_import_on_demand = 999 6 | ij_java_use_single_class_imports = true 7 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | conduct@buf.build. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: Bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | 13 | ## Steps to Reproduce 14 | 20 | 21 | ## Expected Behavior 22 | 23 | 24 | 25 | ## Actual Behavior 26 | 27 | 28 | 29 | ## Screenshots/Logs 30 | 31 | 32 | 33 | ## Environment 34 | 35 | - **Operating System**: 36 | - **Version**: 37 | - **Compiler/Toolchain**: 38 | - **Protobuf Compiler & Version**: 39 | - **Protovalidate Version**: 40 | 41 | ## Possible Solution 42 | 43 | 44 | ## Additional Context 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Feature description:** 11 | 12 | 13 | **Problem it solves or use case:** 14 | 15 | 16 | **Proposed implementation or solution:** 17 | 18 | 19 | **Contribution:** 20 | 21 | 22 | **Examples or references:** 23 | 24 | 25 | **Additional context:** 26 | 27 | -------------------------------------------------------------------------------- /.github/buf-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yaml: -------------------------------------------------------------------------------- 1 | name: Add issues and PRs to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | - transferred 9 | pull_request_target: 10 | types: 11 | - opened 12 | - reopened 13 | issue_comment: 14 | types: 15 | - created 16 | 17 | jobs: 18 | call-workflow-add-to-project: 19 | name: Call workflow to add issue to project 20 | uses: bufbuild/base-workflows/.github/workflows/add-to-project.yaml@main 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ['v*'] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '15 22 * * *' 10 | workflow_dispatch: {} # support manual runs 11 | permissions: 12 | contents: read 13 | jobs: 14 | test: 15 | name: Unit tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 20 | - name: Cache Go Modules 21 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 22 | with: 23 | path: | 24 | ~/.cache/go-build 25 | ~/go/pkg/mod 26 | key: ${{ runner.os }}-gomod-ci-${{ hashFiles('gradle.properties', 'gradle/libs.versions.toml') }} 27 | restore-keys: 28 | ${{ runner.os }}-gomod-ci- 29 | - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 30 | with: 31 | distribution: 'temurin' 32 | java-version: '21' 33 | cache: 'gradle' 34 | - uses: bufbuild/buf-action@8f4a1456a0ab6a1eb80ba68e53832e6fcfacc16c # v1.3.0 35 | with: 36 | setup_only: true 37 | - env: 38 | BUF_TOKEN: ${{ secrets.BUF_TOKEN }} 39 | run: echo ${BUF_TOKEN} | buf registry login buf.build --token-stdin 40 | - name: Validate Gradle Wrapper 41 | uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 42 | - name: Lint 43 | run: make lint 44 | - name: Generate 45 | run: make checkgenerate 46 | - name: Build 47 | run: make build 48 | - name: Docs 49 | run: make docs 50 | - name: Execute tests 51 | run: make test 52 | -------------------------------------------------------------------------------- /.github/workflows/conformance.yaml: -------------------------------------------------------------------------------- 1 | name: Conformance 2 | on: 3 | pull_request: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | branches: 8 | - 'main' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | conformance: 15 | name: Conformance 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 20 | - name: Cache Go Modules 21 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 22 | with: 23 | path: | 24 | ~/.cache/go-build 25 | ~/go/pkg/mod 26 | key: ${{ runner.os }}-gomod-conformance-${{ hashFiles('gradle.properties', 'gradle/libs.versions.toml') }} 27 | restore-keys: 28 | ${{ runner.os }}-gomod-conformance- 29 | - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 30 | with: 31 | distribution: 'temurin' 32 | java-version: '21' 33 | cache: 'gradle' 34 | - uses: bufbuild/buf-action@8f4a1456a0ab6a1eb80ba68e53832e6fcfacc16c # v1.3.0 35 | with: 36 | setup_only: true 37 | - env: 38 | BUF_TOKEN: ${{ secrets.BUF_TOKEN }} 39 | run: echo ${BUF_TOKEN} | buf registry login buf.build --token-stdin 40 | - name: Validate Gradle Wrapper 41 | uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 42 | - name: Test conformance 43 | run: make conformance 44 | -------------------------------------------------------------------------------- /.github/workflows/emergency-review-bypass.yaml: -------------------------------------------------------------------------------- 1 | name: Emergency review bypass 2 | on: 3 | pull_request: 4 | types: 5 | - labeled 6 | permissions: 7 | pull-requests: write 8 | jobs: 9 | approve: 10 | name: Approve 11 | if: github.event.label.name == 'Emergency Bypass Review' 12 | uses: bufbuild/base-workflows/.github/workflows/emergency-review-bypass.yaml@main 13 | secrets: inherit 14 | -------------------------------------------------------------------------------- /.github/workflows/notify-approval-bypass.yaml: -------------------------------------------------------------------------------- 1 | name: Approval bypass notifier 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | branches: 7 | - main 8 | permissions: 9 | pull-requests: read 10 | jobs: 11 | notify: 12 | name: Notify 13 | uses: bufbuild/base-workflows/.github/workflows/notify-approval-bypass.yaml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/pr-hygiene.yaml: -------------------------------------------------------------------------------- 1 | name: PR Hygiene 2 | # Prevent writing to the repository using the CI token. 3 | # Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions 4 | permissions: 5 | pull-requests: read 6 | on: 7 | workflow_call: 8 | pull_request: 9 | # By default, a workflow only runs when a pull_request's activity type is opened, 10 | # synchronize, or reopened. We explicity override here so that PR titles are 11 | # re-linted when the PR text content is edited. 12 | types: 13 | - opened 14 | - edited 15 | - reopened 16 | - synchronize 17 | jobs: 18 | lint-title: 19 | name: Lint title 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Lint title 23 | uses: morrisoncole/pr-lint-action@51f3cfabaf5d46f94e54524214e45685f0401b2a # v1.7.1 24 | with: 25 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 26 | # https://regex101.com/r/I6oK5v/1 27 | title-regex: "^[A-Z].*[^.?!,\\-;:]$" 28 | on-failed-regex-fail-action: true 29 | on-failed-regex-create-review: false 30 | on-failed-regex-request-changes: false 31 | on-failed-regex-comment: "PR titles must start with a capital letter and not end with punctuation." 32 | on-succeeded-regex-dismiss-review-comment: "Thanks for helping keep our PR titles consistent!" 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Gradle Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | workflow_dispatch: {} # support manual runs 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 17 | 18 | - name: Cache Go Modules 19 | uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 20 | with: 21 | path: | 22 | ~/.cache/go-build 23 | ~/go/pkg/mod 24 | key: ${{ runner.os }}-gomod-ci-${{ hashFiles('gradle.properties', 'gradle/libs.versions.toml') }} 25 | restore-keys: 26 | ${{ runner.os }}-gomod-ci- 27 | 28 | - name: Set up JDK 29 | uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 30 | with: 31 | distribution: 'temurin' 32 | java-version: '21' 33 | 34 | - uses: bufbuild/buf-action@8f4a1456a0ab6a1eb80ba68e53832e6fcfacc16c # v1.3.0 35 | with: 36 | setup_only: true 37 | - env: 38 | BUF_TOKEN: ${{ secrets.BUF_TOKEN }} 39 | run: echo ${BUF_TOKEN} | buf registry login buf.build --token-stdin 40 | 41 | - name: Validate Gradle Wrapper 42 | uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 43 | 44 | - name: Configure GPG signing & publish 45 | env: 46 | GPG_KEY: ${{ secrets.GPG_KEY }} 47 | GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} 48 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 49 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 50 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 51 | run: | 52 | export ORG_GRADLE_PROJECT_mavenCentralUsername=$SONATYPE_USER 53 | export ORG_GRADLE_PROJECT_mavenCentralPassword=$SONATYPE_PASSWORD 54 | export ORG_GRADLE_PROJECT_signingInMemoryKeyPassword=$GPG_PASSPHRASE 55 | release_version=$(git describe --tags --abbrev=0 --exact-match) 56 | export ORG_GRADLE_PROJECT_releaseVersion="${release_version:1}" 57 | # https://github.com/keybase/keybase-issues/issues/2798 58 | export GPG_TTY=$(tty) 59 | # Import gpg keys and warm the passphrase to avoid the gpg 60 | # passphrase prompt when initiating a deploy 61 | # `--pinentry-mode=loopback` could be needed to ensure we 62 | # suppress the gpg prompt 63 | echo $GPG_KEY | base64 --decode > signing-key 64 | gpg --passphrase $GPG_PASSPHRASE --batch --import signing-key 65 | export ORG_GRADLE_PROJECT_signingInMemoryKey=$(gpg --armor --passphrase $GPG_PASSPHRASE --pinentry-mode=loopback --export-secret-keys $GPG_KEY_NAME signing-key | grep -v '\-\-' | grep -v '^=.' | tr -d '\n') 66 | shred signing-key 67 | make release 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /**/build/* 2 | !/**/proto/build/* 3 | !/**/src/**/build/* 4 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 5 | !gradle-wrapper.jar 6 | /.gradle 7 | *.iml 8 | /.idea 9 | /.tmp 10 | /bin/ 11 | # Cache of project 12 | /.gradletasknamecache 13 | # Ignore Gradle GUI config 14 | /gradle-app.setting -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # See https://tech.davis-hansson.com/p/make/ 2 | SHELL := bash 3 | .DELETE_ON_ERROR: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | .DEFAULT_GOAL := all 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | MAKEFLAGS += --no-print-directory 9 | GRADLE ?= ./gradlew 10 | 11 | .PHONY: all 12 | all: lint generate build docs conformance ## Run all tests and lint (default) 13 | 14 | .PHONY: build 15 | build: ## Build the entire project. 16 | $(GRADLE) build 17 | 18 | .PHONY: docs 19 | docs: ## Build javadocs for the project. 20 | $(GRADLE) javadoc 21 | 22 | .PHONY: checkgenerate 23 | checkgenerate: generate ## Checks if `make generate` produces a diff. 24 | @# Used in CI to verify that `make generate` doesn't produce a diff. 25 | test -z "$$(git status --porcelain | tee /dev/stderr)" 26 | 27 | .PHONY: clean 28 | clean: ## Delete intermediate build artifacts 29 | $(GRADLE) clean 30 | 31 | .PHONY: conformance 32 | conformance: ## Execute conformance tests. 33 | $(GRADLE) conformance:conformance 34 | 35 | .PHONY: help 36 | help: ## Describe useful make targets 37 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}' 38 | 39 | .PHONY: generate 40 | generate: ## Regenerate code and license headers 41 | $(GRADLE) generate 42 | 43 | .PHONY: lint 44 | lint: ## Lint code 45 | $(GRADLE) spotlessCheck 46 | 47 | .PHONY: lintfix 48 | lintfix: ## Applies the lint changes. 49 | $(GRADLE) spotlessApply 50 | 51 | .PHONY: release 52 | release: ## Upload artifacts to Maven Central. 53 | $(GRADLE) --info publishAndReleaseToMavenCentral --stacktrace --no-daemon --no-parallel --no-configuration-cache 54 | 55 | .PHONY: releaselocal 56 | releaselocal: ## Release artifacts to local maven repository. 57 | $(GRADLE) --info publishToMavenLocal 58 | 59 | .PHONY: test 60 | test: ## Run all tests. 61 | $(GRADLE) test 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![The Buf logo](.github/buf-logo.svg)][buf] 2 | 3 | # protovalidate-java 4 | 5 | [![CI](https://github.com/bufbuild/protovalidate-java/actions/workflows/ci.yaml/badge.svg)](https://github.com/bufbuild/protovalidate-java/actions/workflows/ci.yaml) 6 | [![Conformance](https://github.com/bufbuild/protovalidate-java/actions/workflows/conformance.yaml/badge.svg)](https://github.com/bufbuild/protovalidate-java/actions/workflows/conformance.yaml) 7 | [![BSR](https://img.shields.io/badge/BSR-Module-0C65EC)][buf-mod] 8 | 9 | [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]. 10 | 11 | With Protovalidate, you can annotate your Protobuf messages with both standard and custom validation rules: 12 | 13 | ```protobuf 14 | syntax = "proto3"; 15 | 16 | package acme.user.v1; 17 | 18 | import "buf/validate/validate.proto"; 19 | 20 | message User { 21 | string id = 1 [(buf.validate.field).string.uuid = true]; 22 | uint32 age = 2 [(buf.validate.field).uint32.lte = 150]; // We can only hope. 23 | string email = 3 [(buf.validate.field).string.email = true]; 24 | string first_name = 4 [(buf.validate.field).string.max_len = 64]; 25 | string last_name = 5 [(buf.validate.field).string.max_len = 64]; 26 | 27 | option (buf.validate.message).cel = { 28 | id: "first_name_requires_last_name" 29 | message: "last_name must be present if first_name is present" 30 | expression: "!has(this.first_name) || has(this.last_name)" 31 | }; 32 | } 33 | ``` 34 | 35 | Once you've added `protovalidate-java` to your project, validation is idiomatic Java: 36 | 37 | ```java 38 | ValidationResult result = validator.validate(message); 39 | if (!result.isSuccess()) { 40 | // Handle failure. 41 | } 42 | ``` 43 | 44 | ## Installation 45 | 46 | > [!TIP] 47 | > 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]. 48 | 49 | `protovalidate-java` is listed in [Maven Central][maven], which provides installation snippets for Gradle, Maven, and other package managers. In Gradle, it's: 50 | 51 | ```gradle 52 | dependencies { 53 | implementation 'build.buf:protovalidate:' 54 | } 55 | ``` 56 | 57 | ## Documentation 58 | 59 | Comprehensive documentation for Protovalidate is available at [protovalidate.com][protovalidate]. 60 | 61 | Highlights for Java developers include: 62 | 63 | * The [developer quickstart][quickstart] 64 | * A comprehensive RPC quickstart for [Java and gRPC][grpc-java] 65 | * A [migration guide for protoc-gen-validate][migration-guide] users 66 | 67 | ## Additional languages and repositories 68 | 69 | Protovalidate isn't just for Java! You might be interested in sibling repositories for other languages: 70 | 71 | - [`protovalidate-go`][pv-go] (Go) 72 | - [`protovalidate-python`][pv-python] (Python) 73 | - [`protovalidate-cc`][pv-cc] (C++) 74 | - [`protovalidate-es`][pv-es] (TypeScript and JavaScript) 75 | 76 | Additionally, [protovalidate's core repository](https://github.com/bufbuild/protovalidate) provides: 77 | 78 | - [Protovalidate's Protobuf API][validate-proto] 79 | - [Conformance testing utilities][conformance] for acceptance testing of `protovalidate` implementations 80 | 81 | 82 | ## Contributing 83 | 84 | We genuinely appreciate any help! If you'd like to contribute, check out these resources: 85 | 86 | - [Contributing Guidelines][contributing]: Guidelines to make your contribution process straightforward and meaningful 87 | - [Conformance testing utilities](https://github.com/bufbuild/protovalidate/tree/main/docs/conformance.md): Utilities providing acceptance testing of `protovalidate` implementations 88 | 89 | ## Legal 90 | 91 | Offered under the [Apache 2 license][license]. 92 | 93 | [buf]: https://buf.build 94 | [cel]: https://cel.dev 95 | 96 | [pv-go]: https://github.com/bufbuild/protovalidate-go 97 | [pv-java]: https://github.com/bufbuild/protovalidate-java 98 | [pv-python]: https://github.com/bufbuild/protovalidate-python 99 | [pv-cc]: https://github.com/bufbuild/protovalidate-cc 100 | [pv-es]: https://github.com/bufbuild/protovalidate-es 101 | 102 | [license]: LICENSE 103 | [contributing]: .github/CONTRIBUTING.md 104 | [buf-mod]: https://buf.build/bufbuild/protovalidate 105 | 106 | [protoc-gen-validate]: https://github.com/bufbuild/protoc-gen-validate 107 | 108 | [protovalidate]: https://protovalidate.com 109 | [quickstart]: https://protovalidate.com/quickstart/ 110 | [connect-go]: https://protovalidate.com/quickstart/connect-go/ 111 | [grpc-go]: https://protovalidate.com/quickstart/grpc-go/ 112 | [grpc-java]: https://protovalidate.com/quickstart/grpc-java/ 113 | [grpc-python]: https://protovalidate.com/quickstart/grpc-python/ 114 | [migration-guide]: https://protovalidate.com/migration-guides/migrate-from-protoc-gen-validate/ 115 | 116 | [maven]: https://central.sonatype.com/artifact/build.buf/protovalidate/overview 117 | [pkg-go]: https://pkg.go.dev/github.com/bufbuild/protovalidate-go 118 | 119 | [validate-proto]: https://buf.build/bufbuild/protovalidate/docs/main:buf.validate 120 | [conformance]: https://github.com/bufbuild/protovalidate/blob/main/docs/conformance.md 121 | [examples]: https://github.com/bufbuild/protovalidate/tree/main/examples 122 | [migrate]: https://protovalidate.com/migration-guides/migrate-from-protoc-gen-validate/ 123 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | clean: true 3 | plugins: 4 | - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion 5 | out: build/generated/sources/bufgen 6 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | modules: 3 | - path: src/main/resources 4 | - path: src/test/resources/proto 5 | lint: 6 | use: 7 | - DEFAULT 8 | breaking: 9 | use: 10 | - FILE 11 | -------------------------------------------------------------------------------- /conformance/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | clean: true 3 | managed: 4 | enabled: true 5 | override: 6 | - file_option: java_package_prefix 7 | value: build 8 | plugins: 9 | - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion 10 | out: build/generated/sources/bufgen 11 | -------------------------------------------------------------------------------- /conformance/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.diffplug.gradle.spotless.SpotlessExtension 2 | import net.ltgt.gradle.errorprone.errorprone 3 | 4 | plugins { 5 | `version-catalog` 6 | 7 | application 8 | java 9 | alias(libs.plugins.errorprone) 10 | alias(libs.plugins.osdetector) 11 | } 12 | 13 | // Conformance tests aren't bound by lowest common library version. 14 | java { 15 | sourceCompatibility = JavaVersion.VERSION_21 16 | targetCompatibility = JavaVersion.VERSION_21 17 | } 18 | 19 | val buf: Configuration by configurations.creating 20 | 21 | tasks.register("configureBuf") { 22 | description = "Installs the Buf CLI." 23 | File(buf.asPath).setExecutable(true) 24 | } 25 | 26 | val conformanceCLIFile = 27 | project.layout.buildDirectory 28 | .file("gobin/protovalidate-conformance") 29 | .get() 30 | .asFile 31 | val conformanceCLIPath: String = conformanceCLIFile.absolutePath 32 | val conformanceAppScript: String = 33 | project.layout.buildDirectory 34 | .file("install/conformance/bin/conformance") 35 | .get() 36 | .asFile.absolutePath 37 | val conformanceArgs = (project.findProperty("protovalidate.conformance.args")?.toString() ?: "").split("\\s+".toRegex()) 38 | 39 | tasks.register("installProtovalidateConformance") { 40 | description = "Installs the Protovalidate Conformance CLI." 41 | environment("GOBIN", conformanceCLIFile.parentFile.absolutePath) 42 | outputs.file(conformanceCLIFile) 43 | commandLine( 44 | "go", 45 | "install", 46 | "github.com/bufbuild/protovalidate/tools/protovalidate-conformance@${project.findProperty("protovalidate.version")}", 47 | ) 48 | } 49 | 50 | tasks.register("conformance") { 51 | dependsOn("installDist", "installProtovalidateConformance") 52 | description = "Runs protovalidate conformance tests." 53 | commandLine(*(listOf(conformanceCLIPath) + conformanceArgs + listOf(conformanceAppScript)).toTypedArray()) 54 | } 55 | 56 | tasks.register("filterBufGenYaml") { 57 | from(".") 58 | include("buf.gen.yaml") 59 | includeEmptyDirs = false 60 | into(layout.buildDirectory.dir("buf-gen-templates")) 61 | expand("protocJavaPluginVersion" to "v${libs.versions.protobuf.get().substringAfter('.')}") 62 | filteringCharset = "UTF-8" 63 | } 64 | 65 | tasks.register("generateConformance") { 66 | dependsOn("configureBuf", "filterBufGenYaml") 67 | description = "Generates sources for the bufbuild/protovalidate-testing module to build/generated/sources/bufgen." 68 | commandLine( 69 | buf.asPath, 70 | "generate", 71 | "--template", 72 | "${layout.buildDirectory.get()}/buf-gen-templates/buf.gen.yaml", 73 | "buf.build/bufbuild/protovalidate-testing:${project.findProperty("protovalidate.version")}", 74 | ) 75 | } 76 | 77 | sourceSets { 78 | main { 79 | java { 80 | srcDir(layout.buildDirectory.dir("generated/sources/bufgen")) 81 | } 82 | } 83 | } 84 | 85 | tasks.withType { 86 | dependsOn("generateConformance") 87 | if (JavaVersion.current().isJava9Compatible) { 88 | doFirst { 89 | options.compilerArgs = mutableListOf("--release", "8") 90 | } 91 | } 92 | // Disable errorprone on generated code 93 | options.errorprone.excludedPaths.set(".*/build/generated/sources/bufgen/.*") 94 | } 95 | 96 | // Disable javadoc for conformance tests 97 | tasks.withType { 98 | enabled = false 99 | } 100 | 101 | application { 102 | mainClass.set("build.buf.protovalidate.conformance.Main") 103 | } 104 | 105 | tasks { 106 | jar { 107 | dependsOn(":jar") 108 | manifest { 109 | attributes(mapOf("Main-Class" to "build.buf.protovalidate.conformance.Main")) 110 | } 111 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 112 | // This line of code recursively collects and copies all of a project's files 113 | // and adds them to the JAR itself. One can extend this task, to skip certain 114 | // files or particular types at will 115 | val sourcesMain = sourceSets.main.get() 116 | val contents = 117 | configurations.runtimeClasspath 118 | .get() 119 | .map { if (it.isDirectory) it else zipTree(it) } + 120 | sourcesMain.output 121 | from(contents) 122 | } 123 | } 124 | 125 | apply(plugin = "com.diffplug.spotless") 126 | configure { 127 | java { 128 | targetExclude("build/generated/sources/bufgen/**/*.java") 129 | } 130 | } 131 | 132 | dependencies { 133 | implementation(project(":")) 134 | implementation(libs.errorprone.annotations) 135 | implementation(libs.protobuf.java) 136 | 137 | implementation(libs.assertj) 138 | implementation(platform(libs.junit.bom)) 139 | 140 | buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe") 141 | 142 | testImplementation("org.junit.jupiter:junit-jupiter") 143 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 144 | 145 | errorprone(libs.errorprone.core) 146 | } 147 | -------------------------------------------------------------------------------- /conformance/expected-failures.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conformance/src/main/java/build/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufbuild/protovalidate-java/HEAD/conformance/src/main/java/build/.DS_Store -------------------------------------------------------------------------------- /conformance/src/main/java/build/buf/protovalidate/conformance/FileDescriptorUtil.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate.conformance; 16 | 17 | import com.google.protobuf.DescriptorProtos; 18 | import com.google.protobuf.Descriptors; 19 | import com.google.protobuf.DynamicMessage; 20 | import com.google.protobuf.ExtensionRegistry; 21 | import com.google.protobuf.TypeRegistry; 22 | import java.util.ArrayList; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | class FileDescriptorUtil { 28 | static Map parse( 29 | DescriptorProtos.FileDescriptorSet fileDescriptorSet) 30 | throws Descriptors.DescriptorValidationException { 31 | Map descriptorMap = new HashMap<>(); 32 | Map fileDescriptorMap = 33 | parseFileDescriptors(fileDescriptorSet); 34 | for (Descriptors.FileDescriptor fileDescriptor : fileDescriptorMap.values()) { 35 | for (Descriptors.Descriptor messageType : fileDescriptor.getMessageTypes()) { 36 | descriptorMap.put(messageType.getFullName(), messageType); 37 | } 38 | } 39 | return descriptorMap; 40 | } 41 | 42 | static Map parseFileDescriptors( 43 | DescriptorProtos.FileDescriptorSet fileDescriptorSet) 44 | throws Descriptors.DescriptorValidationException { 45 | Map fileDescriptorProtoMap = new HashMap<>(); 46 | for (DescriptorProtos.FileDescriptorProto fileDescriptorProto : 47 | fileDescriptorSet.getFileList()) { 48 | if (fileDescriptorProtoMap.containsKey(fileDescriptorProto.getName())) { 49 | throw new RuntimeException("duplicate files found."); 50 | } 51 | fileDescriptorProtoMap.put(fileDescriptorProto.getName(), fileDescriptorProto); 52 | } 53 | Map fileDescriptorMap = new HashMap<>(); 54 | for (DescriptorProtos.FileDescriptorProto fileDescriptorProto : 55 | fileDescriptorSet.getFileList()) { 56 | if (fileDescriptorProto.getDependencyList().isEmpty()) { 57 | fileDescriptorMap.put( 58 | fileDescriptorProto.getName(), 59 | Descriptors.FileDescriptor.buildFrom( 60 | fileDescriptorProto, new Descriptors.FileDescriptor[0], false)); 61 | continue; 62 | } 63 | List dependencies = new ArrayList<>(); 64 | for (String dependency : fileDescriptorProto.getDependencyList()) { 65 | if (fileDescriptorMap.get(dependency) != null) { 66 | dependencies.add(fileDescriptorMap.get(dependency)); 67 | } 68 | } 69 | fileDescriptorMap.put( 70 | fileDescriptorProto.getName(), 71 | Descriptors.FileDescriptor.buildFrom( 72 | fileDescriptorProto, dependencies.toArray(new Descriptors.FileDescriptor[0]), false)); 73 | } 74 | return fileDescriptorMap; 75 | } 76 | 77 | static TypeRegistry createTypeRegistry( 78 | Iterable fileDescriptors) { 79 | TypeRegistry.Builder registryBuilder = TypeRegistry.newBuilder(); 80 | for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) { 81 | registryBuilder.add(fileDescriptor.getMessageTypes()); 82 | } 83 | return registryBuilder.build(); 84 | } 85 | 86 | static ExtensionRegistry createExtensionRegistry( 87 | Iterable fileDescriptors) { 88 | ExtensionRegistry registry = ExtensionRegistry.newInstance(); 89 | for (Descriptors.FileDescriptor fileDescriptor : fileDescriptors) { 90 | registerFileExtensions(registry, fileDescriptor); 91 | } 92 | return registry; 93 | } 94 | 95 | private static void registerFileExtensions( 96 | ExtensionRegistry registry, Descriptors.FileDescriptor fileDescriptor) { 97 | registerExtensions(registry, fileDescriptor.getExtensions()); 98 | for (Descriptors.Descriptor descriptor : fileDescriptor.getMessageTypes()) { 99 | registerMessageExtensions(registry, descriptor); 100 | } 101 | } 102 | 103 | private static void registerMessageExtensions( 104 | ExtensionRegistry registry, Descriptors.Descriptor descriptor) { 105 | registerExtensions(registry, descriptor.getExtensions()); 106 | for (Descriptors.Descriptor nestedDescriptor : descriptor.getNestedTypes()) { 107 | registerMessageExtensions(registry, nestedDescriptor); 108 | } 109 | } 110 | 111 | private static void registerExtensions( 112 | ExtensionRegistry registry, List extensions) { 113 | for (Descriptors.FieldDescriptor fieldDescriptor : extensions) { 114 | if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) { 115 | registry.add( 116 | fieldDescriptor, DynamicMessage.getDefaultInstance(fieldDescriptor.getMessageType())); 117 | } else { 118 | registry.add(fieldDescriptor); 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /conformance/src/main/java/build/buf/protovalidate/conformance/Main.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate.conformance; 16 | 17 | import build.buf.protovalidate.Config; 18 | import build.buf.protovalidate.ValidationResult; 19 | import build.buf.protovalidate.Validator; 20 | import build.buf.protovalidate.ValidatorFactory; 21 | import build.buf.protovalidate.exceptions.CompilationException; 22 | import build.buf.protovalidate.exceptions.ExecutionException; 23 | import build.buf.validate.ValidateProto; 24 | import build.buf.validate.Violations; 25 | import build.buf.validate.conformance.harness.TestConformanceRequest; 26 | import build.buf.validate.conformance.harness.TestConformanceResponse; 27 | import build.buf.validate.conformance.harness.TestResult; 28 | import com.google.errorprone.annotations.FormatMethod; 29 | import com.google.protobuf.Any; 30 | import com.google.protobuf.ByteString; 31 | import com.google.protobuf.Descriptors; 32 | import com.google.protobuf.DynamicMessage; 33 | import com.google.protobuf.ExtensionRegistry; 34 | import com.google.protobuf.InvalidProtocolBufferException; 35 | import com.google.protobuf.TypeRegistry; 36 | import java.util.HashMap; 37 | import java.util.Map; 38 | 39 | public class Main { 40 | public static void main(String[] args) { 41 | try { 42 | ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); 43 | extensionRegistry.add(ValidateProto.message); 44 | extensionRegistry.add(ValidateProto.field); 45 | extensionRegistry.add(ValidateProto.oneof); 46 | TestConformanceRequest request = 47 | TestConformanceRequest.parseFrom(System.in, extensionRegistry); 48 | TestConformanceResponse response = testConformance(request); 49 | response.writeTo(System.out); 50 | } catch (Exception e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | 55 | static TestConformanceResponse testConformance(TestConformanceRequest request) { 56 | try { 57 | Map descriptorMap = 58 | FileDescriptorUtil.parse(request.getFdset()); 59 | Map fileDescriptorMap = 60 | FileDescriptorUtil.parseFileDescriptors(request.getFdset()); 61 | TypeRegistry typeRegistry = FileDescriptorUtil.createTypeRegistry(fileDescriptorMap.values()); 62 | ExtensionRegistry extensionRegistry = 63 | FileDescriptorUtil.createExtensionRegistry(fileDescriptorMap.values()); 64 | Config cfg = 65 | Config.newBuilder() 66 | .setTypeRegistry(typeRegistry) 67 | .setExtensionRegistry(extensionRegistry) 68 | .build(); 69 | Validator validator = ValidatorFactory.newBuilder().withConfig(cfg).build(); 70 | 71 | TestConformanceResponse.Builder responseBuilder = TestConformanceResponse.newBuilder(); 72 | Map resultsMap = new HashMap<>(); 73 | for (Map.Entry entry : request.getCasesMap().entrySet()) { 74 | TestResult testResult = testCase(validator, descriptorMap, entry.getValue()); 75 | resultsMap.put(entry.getKey(), testResult); 76 | } 77 | responseBuilder.putAllResults(resultsMap); 78 | return responseBuilder.build(); 79 | } catch (Exception e) { 80 | throw new RuntimeException(e); 81 | } 82 | } 83 | 84 | static TestResult testCase( 85 | Validator validator, Map fileDescriptors, Any testCase) 86 | throws InvalidProtocolBufferException { 87 | String fullName = testCase.getTypeUrl(); 88 | int slash = fullName.indexOf('/'); 89 | if (slash != -1) { 90 | fullName = fullName.substring(slash + 1); 91 | } 92 | Descriptors.Descriptor descriptor = fileDescriptors.get(fullName); 93 | if (descriptor == null) { 94 | return unexpectedErrorResult("Unable to find descriptor: %s", fullName); 95 | } 96 | ByteString testCaseValue = testCase.getValue(); 97 | DynamicMessage dynamicMessage = 98 | DynamicMessage.newBuilder(descriptor).mergeFrom(testCaseValue).build(); 99 | return validate(validator, dynamicMessage); 100 | } 101 | 102 | private static TestResult validate(Validator validator, DynamicMessage dynamicMessage) { 103 | try { 104 | ValidationResult result = validator.validate(dynamicMessage); 105 | if (result.isSuccess()) { 106 | return TestResult.newBuilder().setSuccess(true).build(); 107 | } 108 | Violations error = 109 | Violations.newBuilder().addAllViolations(result.toProto().getViolationsList()).build(); 110 | return TestResult.newBuilder().setValidationError(error).build(); 111 | } catch (CompilationException e) { 112 | return TestResult.newBuilder().setCompilationError(e.getMessage()).build(); 113 | } catch (ExecutionException e) { 114 | return TestResult.newBuilder().setRuntimeError(e.getMessage()).build(); 115 | } catch (Exception e) { 116 | return unexpectedErrorResult("unknown error: %s", e.toString()); 117 | } 118 | } 119 | 120 | @FormatMethod 121 | static TestResult unexpectedErrorResult(String format, Object... args) { 122 | String errorMessage = String.format(format, args); 123 | return TestResult.newBuilder().setUnexpectedError(errorMessage).build(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Version of buf.build/bufbuild/protovalidate to use. 2 | protovalidate.version = v1.1.0 3 | 4 | # Arguments to the protovalidate-conformance CLI 5 | protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml 6 | 7 | # Argument to the license-header CLI 8 | license-header.years = 2023-2025 9 | 10 | # Version of the cel-spec that this implementation is conformant with 11 | cel.spec.version = v0.24.0 12 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | assertj = "3.27.6" 3 | buf = "1.61.0" 4 | cel = "0.11.1" 5 | error-prone = "2.45.0" 6 | junit = "5.14.1" 7 | maven-publish = "0.35.0" 8 | protobuf = "4.33.2" 9 | 10 | [libraries] 11 | assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } 12 | buf = { module = "build.buf:buf", version.ref = "buf" } 13 | cel = { module = "dev.cel:cel", version.ref = "cel" } 14 | errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" } 15 | errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" } 16 | grpc-protobuf = { module = "io.grpc:grpc-protobuf", version = "1.77.0" } 17 | jspecify = { module ="org.jspecify:jspecify", version = "1.0.0" } 18 | junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } 19 | maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } 20 | nullaway = { module = "com.uber.nullaway:nullaway", version = "0.12.14" } 21 | protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } 22 | spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "8.1.0" } 23 | 24 | [plugins] 25 | errorprone = { id = "net.ltgt.errorprone", version = "4.3.0" } 26 | git = { id = "com.palantir.git-version", version = "4.2.0" } 27 | maven = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish" } 28 | osdetector = { id = "com.google.osdetector", version = "1.7.3" } 29 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufbuild/protovalidate-java/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "protovalidate" 2 | include("conformance") 3 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/AnyEvaluator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import build.buf.validate.AnyRules; 19 | import build.buf.validate.FieldPath; 20 | import build.buf.validate.FieldRules; 21 | import com.google.protobuf.Descriptors; 22 | import com.google.protobuf.Message; 23 | import java.util.ArrayList; 24 | import java.util.Collections; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Set; 28 | 29 | /** 30 | * A specialized evaluator for applying {@link build.buf.validate.AnyRules} to an {@link 31 | * com.google.protobuf.Any} message. This is handled outside CEL which attempts to hydrate {@link 32 | * com.google.protobuf.Any}'s within an expression, breaking evaluation if the type is unknown at 33 | * runtime. 34 | */ 35 | final class AnyEvaluator implements Evaluator { 36 | private final RuleViolationHelper helper; 37 | private final Descriptors.FieldDescriptor typeURLDescriptor; 38 | private final Set in; 39 | private final List inValue; 40 | private final Set notIn; 41 | private final List notInValue; 42 | 43 | private static final Descriptors.FieldDescriptor ANY_DESCRIPTOR = 44 | FieldRules.getDescriptor().findFieldByNumber(FieldRules.ANY_FIELD_NUMBER); 45 | 46 | private static final Descriptors.FieldDescriptor IN_DESCRIPTOR = 47 | AnyRules.getDescriptor().findFieldByNumber(AnyRules.IN_FIELD_NUMBER); 48 | 49 | private static final Descriptors.FieldDescriptor NOT_IN_DESCRIPTOR = 50 | AnyRules.getDescriptor().findFieldByNumber(AnyRules.NOT_IN_FIELD_NUMBER); 51 | 52 | private static final FieldPath IN_RULE_PATH = 53 | FieldPath.newBuilder() 54 | .addElements(FieldPathUtils.fieldPathElement(ANY_DESCRIPTOR)) 55 | .addElements(FieldPathUtils.fieldPathElement(IN_DESCRIPTOR)) 56 | .build(); 57 | 58 | private static final FieldPath NOT_IN_RULE_PATH = 59 | FieldPath.newBuilder() 60 | .addElements(FieldPathUtils.fieldPathElement(ANY_DESCRIPTOR)) 61 | .addElements(FieldPathUtils.fieldPathElement(NOT_IN_DESCRIPTOR)) 62 | .build(); 63 | 64 | /** Constructs a new evaluator for {@link build.buf.validate.AnyRules} messages. */ 65 | AnyEvaluator( 66 | ValueEvaluator valueEvaluator, 67 | Descriptors.FieldDescriptor typeURLDescriptor, 68 | List in, 69 | List notIn) { 70 | this.helper = new RuleViolationHelper(valueEvaluator); 71 | this.typeURLDescriptor = typeURLDescriptor; 72 | this.in = stringsToSet(in); 73 | this.inValue = in; 74 | this.notIn = stringsToSet(notIn); 75 | this.notInValue = notIn; 76 | } 77 | 78 | @Override 79 | public List evaluate(Value val, boolean failFast) 80 | throws ExecutionException { 81 | Message anyValue = val.messageValue(); 82 | if (anyValue == null) { 83 | return RuleViolation.NO_VIOLATIONS; 84 | } 85 | List violationList = new ArrayList<>(); 86 | String typeURL = (String) anyValue.getField(typeURLDescriptor); 87 | if (!in.isEmpty() && !in.contains(typeURL)) { 88 | RuleViolation.Builder violation = 89 | RuleViolation.newBuilder() 90 | .addAllRulePathElements(helper.getRulePrefixElements()) 91 | .addAllRulePathElements(IN_RULE_PATH.getElementsList()) 92 | .addFirstFieldPathElement(helper.getFieldPathElement()) 93 | .setRuleId("any.in") 94 | .setMessage("type URL must be in the allow list") 95 | .setFieldValue(new RuleViolation.FieldValue(val)) 96 | .setRuleValue(new RuleViolation.FieldValue(this.inValue, IN_DESCRIPTOR)); 97 | violationList.add(violation); 98 | if (failFast) { 99 | return violationList; 100 | } 101 | } 102 | if (!notIn.isEmpty() && notIn.contains(typeURL)) { 103 | RuleViolation.Builder violation = 104 | RuleViolation.newBuilder() 105 | .addAllRulePathElements(helper.getRulePrefixElements()) 106 | .addAllRulePathElements(NOT_IN_RULE_PATH.getElementsList()) 107 | .addFirstFieldPathElement(helper.getFieldPathElement()) 108 | .setRuleId("any.not_in") 109 | .setMessage("type URL must not be in the block list") 110 | .setFieldValue(new RuleViolation.FieldValue(val)) 111 | .setRuleValue(new RuleViolation.FieldValue(this.notInValue, NOT_IN_DESCRIPTOR)); 112 | violationList.add(violation); 113 | } 114 | return violationList; 115 | } 116 | 117 | @Override 118 | public boolean tautology() { 119 | return in.isEmpty() && notIn.isEmpty(); 120 | } 121 | 122 | /** stringsToMap converts a string list to a set for fast lookup. */ 123 | private static Set stringsToSet(List strings) { 124 | if (strings.isEmpty()) { 125 | return Collections.emptySet(); 126 | } 127 | return new HashSet<>(strings); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/AstExpression.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.CompilationException; 18 | import dev.cel.common.CelAbstractSyntaxTree; 19 | import dev.cel.common.CelValidationException; 20 | import dev.cel.common.CelValidationResult; 21 | import dev.cel.common.types.CelKind; 22 | import dev.cel.compiler.CelCompiler; 23 | 24 | /** {@link AstExpression} is a compiled CEL {@link CelAbstractSyntaxTree}. */ 25 | final class AstExpression { 26 | /** The compiled CEL AST. */ 27 | final CelAbstractSyntaxTree ast; 28 | 29 | /** Contains the original expression from the proto file. */ 30 | final Expression source; 31 | 32 | /** Constructs a new {@link AstExpression}. */ 33 | private AstExpression(CelAbstractSyntaxTree ast, Expression source) { 34 | this.ast = ast; 35 | this.source = source; 36 | } 37 | 38 | /** 39 | * Compiles the given expression to a {@link AstExpression}. 40 | * 41 | * @param cel The CEL compiler. 42 | * @param expr The expression to compile. 43 | * @return The compiled {@link AstExpression}. 44 | * @throws CompilationException if the expression compilation fails. 45 | */ 46 | static AstExpression newAstExpression(CelCompiler cel, Expression expr) 47 | throws CompilationException { 48 | CelValidationResult compileResult = cel.compile(expr.expression); 49 | if (!compileResult.getAllIssues().isEmpty()) { 50 | throw new CompilationException( 51 | "Failed to compile expression " + expr.id + ":\n" + compileResult.getIssueString()); 52 | } 53 | CelAbstractSyntaxTree ast; 54 | try { 55 | ast = compileResult.getAst(); 56 | } catch (CelValidationException e) { 57 | // This will not happen as we checked for issues, and it only throws when 58 | // it has at least one issue of error severity. 59 | throw new CompilationException( 60 | "Failed to compile expression " + expr.id + ":\n" + compileResult.getIssueString()); 61 | } 62 | CelKind outKind = ast.getResultType().kind(); 63 | if (outKind != CelKind.BOOL && outKind != CelKind.STRING) { 64 | throw new CompilationException( 65 | String.format( 66 | "Expression outputs, wanted either bool or string: %s %s", expr.id, outKind)); 67 | } 68 | return new AstExpression(ast, expr); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/CelPrograms.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import dev.cel.runtime.CelVariableResolver; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import org.jspecify.annotations.Nullable; 22 | 23 | /** Evaluator that executes a {@link CompiledProgram}. */ 24 | final class CelPrograms implements Evaluator { 25 | private final RuleViolationHelper helper; 26 | 27 | /** A list of {@link CompiledProgram} that will be executed against the input message. */ 28 | private final List programs; 29 | 30 | /** 31 | * Constructs a new {@link CelPrograms}. 32 | * 33 | * @param compiledPrograms The programs to execute. 34 | */ 35 | CelPrograms(@Nullable ValueEvaluator valueEvaluator, List compiledPrograms) { 36 | this.helper = new RuleViolationHelper(valueEvaluator); 37 | this.programs = compiledPrograms; 38 | } 39 | 40 | @Override 41 | public boolean tautology() { 42 | return programs.isEmpty(); 43 | } 44 | 45 | @Override 46 | public List evaluate(Value val, boolean failFast) 47 | throws ExecutionException { 48 | CelVariableResolver bindings = Variable.newThisVariable(val.value(Object.class)); 49 | List violations = new ArrayList<>(); 50 | for (CompiledProgram program : programs) { 51 | RuleViolation.Builder violation = program.eval(val, bindings); 52 | if (violation != null) { 53 | violations.add(violation); 54 | if (failFast) { 55 | break; 56 | } 57 | } 58 | } 59 | return FieldPathUtils.updatePaths( 60 | violations, helper.getFieldPathElement(), helper.getRulePrefixElements()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/CompiledProgram.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import build.buf.validate.FieldPath; 19 | import dev.cel.runtime.CelEvaluationException; 20 | import dev.cel.runtime.CelRuntime.Program; 21 | import dev.cel.runtime.CelVariableResolver; 22 | import org.jspecify.annotations.Nullable; 23 | 24 | /** 25 | * {@link CompiledProgram} is a parsed and type-checked {@link Program} along with the source {@link 26 | * Expression}. 27 | */ 28 | final class CompiledProgram { 29 | /** A compiled CEL program that can be evaluated against a set of variable bindings. */ 30 | private final Program program; 31 | 32 | /** The original expression that was compiled into the program from the proto file. */ 33 | private final Expression source; 34 | 35 | /** The field path from FieldRules to the rule value. */ 36 | @Nullable private final FieldPath rulePath; 37 | 38 | /** The rule value. */ 39 | @Nullable private final Value ruleValue; 40 | 41 | /** 42 | * Global variables to pass to the evaluation step. Program/CelRuntime doesn't have a concept of 43 | * global variables. 44 | */ 45 | @Nullable private final CelVariableResolver globals; 46 | 47 | /** 48 | * Constructs a new {@link CompiledProgram}. 49 | * 50 | * @param program The compiled CEL program. 51 | * @param source The original expression that was compiled into the program. 52 | * @param rulePath The field path from the FieldRules to the rule value. 53 | * @param ruleValue The rule value. 54 | */ 55 | CompiledProgram( 56 | Program program, 57 | Expression source, 58 | @Nullable FieldPath rulePath, 59 | @Nullable Value ruleValue, 60 | @Nullable CelVariableResolver globals) { 61 | this.program = program; 62 | this.source = source; 63 | this.rulePath = rulePath; 64 | this.ruleValue = ruleValue; 65 | this.globals = globals; 66 | } 67 | 68 | /** 69 | * Evaluate the compiled program with a given set of {@link Variable} variables. 70 | * 71 | * @param variables Variables used for the evaluation. 72 | * @param fieldValue Field value to return in violations. 73 | * @return The {@link build.buf.validate.Violation} from the evaluation, or null if there are no 74 | * violations. 75 | * @throws ExecutionException If the evaluation of the CEL program fails with an error. 76 | */ 77 | RuleViolation.@Nullable Builder eval(Value fieldValue, CelVariableResolver variables) 78 | throws ExecutionException { 79 | Object value; 80 | try { 81 | if (this.globals != null) { 82 | variables = CelVariableResolver.hierarchicalVariableResolver(variables, this.globals); 83 | } 84 | value = program.eval(variables); 85 | } catch (CelEvaluationException e) { 86 | throw new ExecutionException(String.format("error evaluating %s: %s", source.id, e)); 87 | } 88 | if (value instanceof String) { 89 | if ("".equals(value)) { 90 | return null; 91 | } 92 | RuleViolation.Builder builder = 93 | RuleViolation.newBuilder().setRuleId(this.source.id).setMessage(value.toString()); 94 | if (fieldValue.fieldDescriptor() != null) { 95 | builder.setFieldValue(new RuleViolation.FieldValue(fieldValue)); 96 | } 97 | if (rulePath != null) { 98 | builder.addAllRulePathElements(rulePath.getElementsList()); 99 | } 100 | if (ruleValue != null && ruleValue.fieldDescriptor() != null) { 101 | builder.setRuleValue(new RuleViolation.FieldValue(ruleValue)); 102 | } 103 | return builder; 104 | } else if (value instanceof Boolean) { 105 | if (Boolean.TRUE.equals(value)) { 106 | return null; 107 | } 108 | String message = this.source.message; 109 | if (message.isEmpty()) { 110 | message = String.format("\"%s\" returned false", this.source.expression); 111 | } 112 | RuleViolation.Builder builder = 113 | RuleViolation.newBuilder().setRuleId(this.source.id).setMessage(message); 114 | if (rulePath != null) { 115 | builder.addAllRulePathElements(rulePath.getElementsList()); 116 | } 117 | if (ruleValue != null && ruleValue.fieldDescriptor() != null) { 118 | builder.setRuleValue(new RuleViolation.FieldValue(ruleValue)); 119 | } 120 | return builder; 121 | } else { 122 | throw new ExecutionException(String.format("resolved to an unexpected type %s", value)); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Config.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import com.google.protobuf.ExtensionRegistry; 18 | import com.google.protobuf.TypeRegistry; 19 | 20 | /** Config is the configuration for a Validator. */ 21 | public final class Config { 22 | private static final TypeRegistry DEFAULT_TYPE_REGISTRY = TypeRegistry.getEmptyTypeRegistry(); 23 | private static final ExtensionRegistry DEFAULT_EXTENSION_REGISTRY = 24 | ExtensionRegistry.getEmptyRegistry(); 25 | 26 | private final boolean failFast; 27 | private final TypeRegistry typeRegistry; 28 | private final ExtensionRegistry extensionRegistry; 29 | private final boolean allowUnknownFields; 30 | 31 | private Config( 32 | boolean failFast, 33 | TypeRegistry typeRegistry, 34 | ExtensionRegistry extensionRegistry, 35 | boolean allowUnknownFields) { 36 | this.failFast = failFast; 37 | this.typeRegistry = typeRegistry; 38 | this.extensionRegistry = extensionRegistry; 39 | this.allowUnknownFields = allowUnknownFields; 40 | } 41 | 42 | /** 43 | * Create a new Configuration builder. 44 | * 45 | * @return a new Configuration builder. 46 | */ 47 | public static Builder newBuilder() { 48 | return new Builder(); 49 | } 50 | 51 | /** 52 | * Checks if the configuration for failing fast is enabled. 53 | * 54 | * @return if failing fast is enabled 55 | */ 56 | public boolean isFailFast() { 57 | return failFast; 58 | } 59 | 60 | /** 61 | * Gets the type registry used for reparsing protobuf messages. 62 | * 63 | * @return a type registry 64 | */ 65 | public TypeRegistry getTypeRegistry() { 66 | return typeRegistry; 67 | } 68 | 69 | /** 70 | * Gets the extension registry used for resolving unknown protobuf extensions. 71 | * 72 | * @return an extension registry 73 | */ 74 | public ExtensionRegistry getExtensionRegistry() { 75 | return extensionRegistry; 76 | } 77 | 78 | /** 79 | * Checks if the configuration for allowing unknown rule fields is enabled. 80 | * 81 | * @return if allowing unknown rule fields is enabled 82 | */ 83 | public boolean isAllowingUnknownFields() { 84 | return allowUnknownFields; 85 | } 86 | 87 | /** Builder for configuration. Provides a forward compatible API for users. */ 88 | public static final class Builder { 89 | private boolean failFast; 90 | private TypeRegistry typeRegistry = DEFAULT_TYPE_REGISTRY; 91 | private ExtensionRegistry extensionRegistry = DEFAULT_EXTENSION_REGISTRY; 92 | private boolean allowUnknownFields; 93 | 94 | private Builder() {} 95 | 96 | /** 97 | * Set the configuration for failing fast. 98 | * 99 | * @param failFast the boolean for enabling 100 | * @return this builder 101 | */ 102 | public Builder setFailFast(boolean failFast) { 103 | this.failFast = failFast; 104 | return this; 105 | } 106 | 107 | /** 108 | * Set the type registry for reparsing protobuf messages. This option should be set alongside 109 | * setExtensionRegistry to allow dynamic resolution of predefined rule extensions. It should be 110 | * set to a TypeRegistry with all the message types from your file descriptor set registered. By 111 | * default, if any unknown field rules are found, compilation of the rules will fail; use 112 | * setAllowUnknownFields to control this behavior. 113 | * 114 | *

Note that the message types for any extensions in setExtensionRegistry must be present in 115 | * the typeRegistry, and have an exactly-equal Descriptor. If the type registry is not set, the 116 | * extension types in the extension registry must have exactly-equal Descriptor types to the 117 | * protovalidate built-in messages. If these conditions are not met, extensions will not be 118 | * resolved as expected. These conditions will be met when constructing a TypeRegistry and 119 | * ExtensionRegistry using information from the same file descriptor sets. 120 | * 121 | * @param typeRegistry the type registry to use 122 | * @return this builder 123 | */ 124 | public Builder setTypeRegistry(TypeRegistry typeRegistry) { 125 | this.typeRegistry = typeRegistry; 126 | return this; 127 | } 128 | 129 | /** 130 | * Set the extension registry for resolving unknown extensions. This option should be set 131 | * alongside setTypeRegistry to allow dynamic resolution of predefined rule extensions. It 132 | * should be set to an ExtensionRegistry with all the extension types from your file descriptor 133 | * set registered. By default, if any unknown field rules are found, compilation of the rules 134 | * will fail; use setAllowUnknownFields to control this behavior. 135 | * 136 | * @param extensionRegistry the extension registry to use 137 | * @return this builder 138 | */ 139 | public Builder setExtensionRegistry(ExtensionRegistry extensionRegistry) { 140 | this.extensionRegistry = extensionRegistry; 141 | return this; 142 | } 143 | 144 | /** 145 | * Set whether unknown rule fields are allowed. If this setting is set to true, unknown standard 146 | * predefined field rules and predefined field rule extensions will be ignored. This setting 147 | * defaults to false, which will result in a CompilationException being thrown whenever an 148 | * unknown field rule is encountered. Setting this to true will cause some field rules to be 149 | * ignored; if the descriptor is dynamic, you can instead use setExtensionRegistry to provide 150 | * dynamic type information that protovalidate can use to resolve the unknown fields. 151 | * 152 | * @param allowUnknownFields setting to apply 153 | * @return this builder 154 | */ 155 | public Builder setAllowUnknownFields(boolean allowUnknownFields) { 156 | this.allowUnknownFields = allowUnknownFields; 157 | return this; 158 | } 159 | 160 | /** 161 | * Build the corresponding {@link Config}. 162 | * 163 | * @return the configuration. 164 | */ 165 | public Config build() { 166 | return new Config(failFast, typeRegistry, extensionRegistry, allowUnknownFields); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/CustomDeclarations.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import static dev.cel.common.CelFunctionDecl.newFunctionDeclaration; 18 | import static dev.cel.common.CelOverloadDecl.newGlobalOverload; 19 | import static dev.cel.common.CelOverloadDecl.newMemberOverload; 20 | 21 | import dev.cel.common.CelFunctionDecl; 22 | import dev.cel.common.CelOverloadDecl; 23 | import dev.cel.common.types.CelType; 24 | import dev.cel.common.types.ListType; 25 | import dev.cel.common.types.SimpleType; 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | import java.util.Collections; 29 | import java.util.List; 30 | import java.util.Locale; 31 | 32 | /** Defines custom declaration functions. */ 33 | final class CustomDeclarations { 34 | 35 | /** 36 | * Create the custom function declaration list. 37 | * 38 | * @return the list of function declarations. 39 | */ 40 | static List create() { 41 | List decls = new ArrayList<>(); 42 | 43 | // Add 'getField' function declaration 44 | decls.add( 45 | newFunctionDeclaration( 46 | "getField", 47 | newGlobalOverload( 48 | "get_field_any_string", 49 | SimpleType.DYN, 50 | Arrays.asList(SimpleType.ANY, SimpleType.STRING)))); 51 | // Add 'isIp' function declaration 52 | decls.add( 53 | newFunctionDeclaration( 54 | "isIp", 55 | newMemberOverload( 56 | "is_ip", SimpleType.BOOL, Arrays.asList(SimpleType.STRING, SimpleType.INT)), 57 | newMemberOverload( 58 | "is_ip_unary", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); 59 | 60 | // Add 'isIpPrefix' function declaration 61 | decls.add( 62 | newFunctionDeclaration( 63 | "isIpPrefix", 64 | newMemberOverload( 65 | "is_ip_prefix_int_bool", 66 | SimpleType.BOOL, 67 | Arrays.asList(SimpleType.STRING, SimpleType.INT, SimpleType.BOOL)), 68 | newMemberOverload( 69 | "is_ip_prefix_int", 70 | SimpleType.BOOL, 71 | Arrays.asList(SimpleType.STRING, SimpleType.INT)), 72 | newMemberOverload( 73 | "is_ip_prefix_bool", 74 | SimpleType.BOOL, 75 | Arrays.asList(SimpleType.STRING, SimpleType.BOOL)), 76 | newMemberOverload( 77 | "is_ip_prefix", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); 78 | 79 | // Add 'isUriRef' function declaration 80 | decls.add( 81 | newFunctionDeclaration( 82 | "isUriRef", 83 | newMemberOverload( 84 | "is_uri_ref", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); 85 | 86 | // Add 'isUri' function declaration 87 | decls.add( 88 | newFunctionDeclaration( 89 | "isUri", 90 | newMemberOverload( 91 | "is_uri", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); 92 | 93 | // Add 'isEmail' function declaration 94 | decls.add( 95 | newFunctionDeclaration( 96 | "isEmail", 97 | newMemberOverload( 98 | "is_email", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); 99 | 100 | // Add 'isHostname' function declaration 101 | decls.add( 102 | newFunctionDeclaration( 103 | "isHostname", 104 | newMemberOverload( 105 | "is_hostname", SimpleType.BOOL, Collections.singletonList(SimpleType.STRING)))); 106 | 107 | decls.add( 108 | newFunctionDeclaration( 109 | "isHostAndPort", 110 | newMemberOverload( 111 | "string_bool_is_host_and_port_bool", 112 | SimpleType.BOOL, 113 | Arrays.asList(SimpleType.STRING, SimpleType.BOOL)))); 114 | 115 | // Add 'startsWith' function declaration 116 | decls.add( 117 | newFunctionDeclaration( 118 | "startsWith", 119 | newMemberOverload( 120 | "starts_with_bytes", 121 | SimpleType.BOOL, 122 | Arrays.asList(SimpleType.BYTES, SimpleType.BYTES)))); 123 | 124 | // Add 'endsWith' function declaration 125 | decls.add( 126 | newFunctionDeclaration( 127 | "endsWith", 128 | newMemberOverload( 129 | "ends_with_bytes", 130 | SimpleType.BOOL, 131 | Arrays.asList(SimpleType.BYTES, SimpleType.BYTES)))); 132 | 133 | // Add 'contains' function declaration 134 | decls.add( 135 | newFunctionDeclaration( 136 | "contains", 137 | newMemberOverload( 138 | "contains_bytes", 139 | SimpleType.BOOL, 140 | Arrays.asList(SimpleType.BYTES, SimpleType.BYTES)))); 141 | 142 | // Add 'isNan' function declaration 143 | decls.add( 144 | newFunctionDeclaration( 145 | "isNan", 146 | newMemberOverload( 147 | "is_nan", SimpleType.BOOL, Collections.singletonList(SimpleType.DOUBLE)))); 148 | 149 | // Add 'isInf' function declaration 150 | decls.add( 151 | newFunctionDeclaration( 152 | "isInf", 153 | newMemberOverload( 154 | "is_inf_unary", SimpleType.BOOL, Collections.singletonList(SimpleType.DOUBLE)), 155 | newMemberOverload( 156 | "is_inf_binary", 157 | SimpleType.BOOL, 158 | Arrays.asList(SimpleType.DOUBLE, SimpleType.INT)))); 159 | 160 | // Add 'unique' function declaration 161 | List uniqueOverloads = new ArrayList<>(); 162 | for (CelType type : 163 | Arrays.asList( 164 | SimpleType.STRING, 165 | SimpleType.INT, 166 | SimpleType.UINT, 167 | SimpleType.DOUBLE, 168 | SimpleType.BYTES, 169 | SimpleType.BOOL)) { 170 | uniqueOverloads.add( 171 | newMemberOverload( 172 | String.format("unique_list_%s", type.name().toLowerCase(Locale.US)), 173 | SimpleType.BOOL, 174 | Collections.singletonList(ListType.create(type)))); 175 | } 176 | decls.add(newFunctionDeclaration("unique", uniqueOverloads)); 177 | 178 | // Add 'format' function declaration 179 | List formatOverloads = new ArrayList<>(); 180 | formatOverloads.add( 181 | newMemberOverload( 182 | "format_list_dyn", 183 | SimpleType.STRING, 184 | Arrays.asList(SimpleType.STRING, ListType.create(SimpleType.DYN)))); 185 | 186 | decls.add(newFunctionDeclaration("format", formatOverloads)); 187 | 188 | return Collections.unmodifiableList(decls); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/EmbeddedMessageEvaluator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import java.util.Collections; 19 | import java.util.List; 20 | 21 | final class EmbeddedMessageEvaluator implements Evaluator { 22 | private final RuleViolationHelper helper; 23 | private final MessageEvaluator messageEvaluator; 24 | 25 | EmbeddedMessageEvaluator(ValueEvaluator valueEvaluator, MessageEvaluator messageEvaluator) { 26 | this.helper = new RuleViolationHelper(valueEvaluator); 27 | this.messageEvaluator = messageEvaluator; 28 | } 29 | 30 | @Override 31 | public boolean tautology() { 32 | return messageEvaluator.tautology(); 33 | } 34 | 35 | @Override 36 | public List evaluate(Value val, boolean failFast) 37 | throws ExecutionException { 38 | return FieldPathUtils.updatePaths( 39 | messageEvaluator.evaluate(val, failFast), 40 | helper.getFieldPathElement(), 41 | Collections.emptyList()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/EnumEvaluator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import build.buf.validate.EnumRules; 19 | import build.buf.validate.FieldPath; 20 | import build.buf.validate.FieldRules; 21 | import com.google.protobuf.Descriptors; 22 | import java.util.Collections; 23 | import java.util.List; 24 | import java.util.Set; 25 | import java.util.stream.Collectors; 26 | 27 | /** 28 | * {@link EnumEvaluator} checks an enum value being a member of the defined values exclusively. This 29 | * check is handled outside CEL as enums are completely type erased to integers. 30 | */ 31 | final class EnumEvaluator implements Evaluator { 32 | private final RuleViolationHelper helper; 33 | 34 | /** Captures all the defined values for this enum */ 35 | private final Set values; 36 | 37 | private static final Descriptors.FieldDescriptor DEFINED_ONLY_DESCRIPTOR = 38 | EnumRules.getDescriptor().findFieldByNumber(EnumRules.DEFINED_ONLY_FIELD_NUMBER); 39 | 40 | private static final FieldPath DEFINED_ONLY_RULE_PATH = 41 | FieldPath.newBuilder() 42 | .addElements( 43 | FieldPathUtils.fieldPathElement( 44 | FieldRules.getDescriptor().findFieldByNumber(FieldRules.ENUM_FIELD_NUMBER))) 45 | .addElements(FieldPathUtils.fieldPathElement(DEFINED_ONLY_DESCRIPTOR)) 46 | .build(); 47 | 48 | /** 49 | * Constructs a new evaluator for enum values. 50 | * 51 | * @param valueDescriptors the list of {@link Descriptors.EnumValueDescriptor} for the enum. 52 | */ 53 | EnumEvaluator( 54 | ValueEvaluator valueEvaluator, List valueDescriptors) { 55 | this.helper = new RuleViolationHelper(valueEvaluator); 56 | if (valueDescriptors.isEmpty()) { 57 | this.values = Collections.emptySet(); 58 | } else { 59 | this.values = 60 | valueDescriptors.stream().map(it -> (long) it.getNumber()).collect(Collectors.toSet()); 61 | } 62 | } 63 | 64 | @Override 65 | public boolean tautology() { 66 | return false; 67 | } 68 | 69 | /** 70 | * Evaluates an enum value. 71 | * 72 | * @param val the value to evaluate. 73 | * @param failFast indicates if the evaluation should stop on the first violation. 74 | * @return the {@link ValidationResult} of the evaluation. 75 | * @throws ExecutionException if an error occurs during the evaluation. 76 | */ 77 | @Override 78 | public List evaluate(Value val, boolean failFast) 79 | throws ExecutionException { 80 | Object enumValue = val.value(Object.class); 81 | if (enumValue == null) { 82 | return RuleViolation.NO_VIOLATIONS; 83 | } 84 | if (!values.contains(enumValue)) { 85 | return Collections.singletonList( 86 | RuleViolation.newBuilder() 87 | .addAllRulePathElements(helper.getRulePrefixElements()) 88 | .addAllRulePathElements(DEFINED_ONLY_RULE_PATH.getElementsList()) 89 | .addFirstFieldPathElement(helper.getFieldPathElement()) 90 | .setRuleId("enum.defined_only") 91 | .setMessage("value must be one of the defined enum values") 92 | .setFieldValue(new RuleViolation.FieldValue(val)) 93 | .setRuleValue(new RuleViolation.FieldValue(true, DEFINED_ONLY_DESCRIPTOR))); 94 | } 95 | return RuleViolation.NO_VIOLATIONS; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Evaluator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import java.util.List; 19 | 20 | /** 21 | * {@link Evaluator} defines a validation evaluator. evaluator implementations may elide type 22 | * checking of the passed in value, as the types have been guaranteed during the build phase. 23 | */ 24 | interface Evaluator { 25 | /** 26 | * Tautology returns true if the evaluator always succeeds. 27 | * 28 | * @return True if the evaluator always succeeds. 29 | */ 30 | boolean tautology(); 31 | 32 | /** 33 | * Checks that the provided val is valid. Unless failFast is true, evaluation attempts to find all 34 | * {@link RuleViolation} present in val instead of returning only the first {@link RuleViolation}. 35 | * 36 | * @param val The value to validate. 37 | * @param failFast If true, validation stops after the first failure. 38 | * @return The result of validation on the specified value. 39 | * @throws ExecutionException If evaluation fails to complete. 40 | */ 41 | List evaluate(Value val, boolean failFast) throws ExecutionException; 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Expression.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.validate.Rule; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** Expression represents a single CEL expression. */ 22 | final class Expression { 23 | /** The id of the rule. */ 24 | final String id; 25 | 26 | /** The message of the rule. */ 27 | final String message; 28 | 29 | /** The expression of the rule. */ 30 | final String expression; 31 | 32 | /** 33 | * Constructs a new Expression. 34 | * 35 | * @param id The ID of the rule. 36 | * @param message The message of the rule. 37 | * @param expression The expression of the rule. 38 | */ 39 | private Expression(String id, String message, String expression) { 40 | this.id = id; 41 | this.message = message; 42 | this.expression = expression; 43 | } 44 | 45 | /** 46 | * Constructs a new Expression from the given rule. 47 | * 48 | * @param rule The rule to create the expression from. 49 | */ 50 | private Expression(Rule rule) { 51 | this(rule.getId(), rule.getMessage(), rule.getExpression()); 52 | } 53 | 54 | /** 55 | * Constructs a new list of {@link Expression} from the given list of rules. 56 | * 57 | * @param rules The list of rules. 58 | * @return The list of expressions. 59 | */ 60 | static List fromRules(List rules) { 61 | List expressions = new ArrayList<>(); 62 | for (build.buf.validate.Rule rule : rules) { 63 | expressions.add(new Expression(rule)); 64 | } 65 | return expressions; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/FieldEvaluator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import build.buf.validate.FieldPath; 19 | import build.buf.validate.FieldRules; 20 | import build.buf.validate.Ignore; 21 | import com.google.protobuf.Descriptors.FieldDescriptor; 22 | import com.google.protobuf.Message; 23 | import java.util.Collections; 24 | import java.util.List; 25 | 26 | /** Performs validation on a single message field, defined by its descriptor. */ 27 | final class FieldEvaluator implements Evaluator { 28 | private static final FieldDescriptor REQUIRED_DESCRIPTOR = 29 | FieldRules.getDescriptor().findFieldByNumber(FieldRules.REQUIRED_FIELD_NUMBER); 30 | 31 | private static final FieldPath REQUIRED_RULE_PATH = 32 | FieldPath.newBuilder() 33 | .addElements(FieldPathUtils.fieldPathElement(REQUIRED_DESCRIPTOR)) 34 | .build(); 35 | 36 | private final RuleViolationHelper helper; 37 | 38 | /** The {@link ValueEvaluator} to apply to the field's value */ 39 | final ValueEvaluator valueEvaluator; 40 | 41 | /** The {@link FieldDescriptor} targeted by this evaluator */ 42 | private final FieldDescriptor descriptor; 43 | 44 | /** Indicates that the field must have a set value. */ 45 | private final boolean required; 46 | 47 | /** Whether validation should be ignored for certain conditions */ 48 | private final Ignore ignore; 49 | 50 | /** Whether the field distinguishes between unpopulated and default values. */ 51 | private final boolean hasPresence; 52 | 53 | /** Constructs a new {@link FieldEvaluator} */ 54 | FieldEvaluator( 55 | ValueEvaluator valueEvaluator, 56 | FieldDescriptor descriptor, 57 | boolean required, 58 | boolean hasPresence, 59 | Ignore ignore) { 60 | this.helper = new RuleViolationHelper(valueEvaluator); 61 | this.valueEvaluator = valueEvaluator; 62 | this.descriptor = descriptor; 63 | this.required = required; 64 | this.hasPresence = hasPresence; 65 | this.ignore = ignore; 66 | } 67 | 68 | @Override 69 | public boolean tautology() { 70 | return !required && valueEvaluator.tautology(); 71 | } 72 | 73 | /** 74 | * Returns whether a field should always skip validation. 75 | * 76 | *

If true, this will take precedence and all checks are skipped. 77 | */ 78 | private boolean shouldIgnoreAlways() { 79 | return this.ignore == Ignore.IGNORE_ALWAYS; 80 | } 81 | 82 | /** 83 | * Returns whether a field should skip validation on its zero value. 84 | * 85 | *

This is generally true for nullable fields or fields with the ignore_empty rule explicitly 86 | * set. 87 | */ 88 | private boolean shouldIgnoreEmpty() { 89 | return this.hasPresence || this.ignore == Ignore.IGNORE_IF_ZERO_VALUE; 90 | } 91 | 92 | @Override 93 | public List evaluate(Value val, boolean failFast) 94 | throws ExecutionException { 95 | if (this.shouldIgnoreAlways()) { 96 | return RuleViolation.NO_VIOLATIONS; 97 | } 98 | Message message = val.messageValue(); 99 | if (message == null) { 100 | return RuleViolation.NO_VIOLATIONS; 101 | } 102 | boolean hasField; 103 | if (descriptor.isRepeated()) { 104 | hasField = message.getRepeatedFieldCount(descriptor) != 0; 105 | } else { 106 | hasField = message.hasField(descriptor); 107 | } 108 | if (required && !hasField) { 109 | return Collections.singletonList( 110 | RuleViolation.newBuilder() 111 | .addFirstFieldPathElement(FieldPathUtils.fieldPathElement(descriptor)) 112 | .addAllRulePathElements(helper.getRulePrefixElements()) 113 | .addAllRulePathElements(REQUIRED_RULE_PATH.getElementsList()) 114 | .setRuleId("required") 115 | .setMessage("value is required") 116 | .setRuleValue(new RuleViolation.FieldValue(true, REQUIRED_DESCRIPTOR))); 117 | } 118 | if (this.shouldIgnoreEmpty() && !hasField) { 119 | return RuleViolation.NO_VIOLATIONS; 120 | } 121 | return valueEvaluator.evaluate( 122 | new ObjectValue(descriptor, message.getField(descriptor)), failFast); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/FieldPathUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.validate.FieldPath; 18 | import build.buf.validate.FieldPathElement; 19 | import com.google.protobuf.Descriptors; 20 | import java.util.List; 21 | import org.jspecify.annotations.Nullable; 22 | 23 | /** Utility class for manipulating error paths in violations. */ 24 | final class FieldPathUtils { 25 | private FieldPathUtils() {} 26 | 27 | /** 28 | * Converts the provided field path to a string. 29 | * 30 | * @param fieldPath A field path to convert to a string. 31 | * @return The string representation of the provided field path. 32 | */ 33 | static String fieldPathString(FieldPath fieldPath) { 34 | StringBuilder builder = new StringBuilder(); 35 | for (FieldPathElement element : fieldPath.getElementsList()) { 36 | if (builder.length() > 0) { 37 | builder.append("."); 38 | } 39 | builder.append(element.getFieldName()); 40 | switch (element.getSubscriptCase()) { 41 | case INDEX: 42 | builder.append("["); 43 | builder.append(element.getIndex()); 44 | builder.append("]"); 45 | break; 46 | case BOOL_KEY: 47 | if (element.getBoolKey()) { 48 | builder.append("[true]"); 49 | } else { 50 | builder.append("[false]"); 51 | } 52 | break; 53 | case INT_KEY: 54 | builder.append("["); 55 | builder.append(element.getIntKey()); 56 | builder.append("]"); 57 | break; 58 | case UINT_KEY: 59 | builder.append("["); 60 | builder.append(element.getUintKey()); 61 | builder.append("]"); 62 | break; 63 | case STRING_KEY: 64 | builder.append("[\""); 65 | builder.append(element.getStringKey().replace("\\", "\\\\").replace("\"", "\\\"")); 66 | builder.append("\"]"); 67 | break; 68 | case SUBSCRIPT_NOT_SET: 69 | break; 70 | } 71 | } 72 | return builder.toString(); 73 | } 74 | 75 | /** 76 | * Returns the field path element that refers to the provided field descriptor. 77 | * 78 | * @param fieldDescriptor The field descriptor to generate a field path element for. 79 | * @return The field path element that corresponds to the provided field descriptor. 80 | */ 81 | static FieldPathElement fieldPathElement(Descriptors.FieldDescriptor fieldDescriptor) { 82 | String name; 83 | if (fieldDescriptor.isExtension()) { 84 | name = "[" + fieldDescriptor.getFullName() + "]"; 85 | } else { 86 | name = fieldDescriptor.getName(); 87 | } 88 | return FieldPathElement.newBuilder() 89 | .setFieldNumber(fieldDescriptor.getNumber()) 90 | .setFieldName(name) 91 | .setFieldType(fieldDescriptor.getType().toProto()) 92 | .build(); 93 | } 94 | 95 | /** 96 | * Provided a list of violations, adjusts it by prepending rule and field path elements. 97 | * 98 | * @param violations A list of violations. 99 | * @param fieldPathElement A field path element to prepend, or null. 100 | * @param rulePathElements Rule path elements to prepend. 101 | * @return For convenience, the list of violations passed into the violations parameter. 102 | */ 103 | static List updatePaths( 104 | List violations, 105 | @Nullable FieldPathElement fieldPathElement, 106 | List rulePathElements) { 107 | if (fieldPathElement != null || !rulePathElements.isEmpty()) { 108 | for (RuleViolation.Builder violation : violations) { 109 | for (int i = rulePathElements.size() - 1; i >= 0; i--) { 110 | violation.addFirstRulePathElement(rulePathElements.get(i)); 111 | } 112 | if (fieldPathElement != null) { 113 | violation.addFirstFieldPathElement(fieldPathElement); 114 | } 115 | } 116 | } 117 | return violations; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Ipv4.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * Ipv4 is a class used to parse a given string to determine if it is an IPv4 address or address 22 | * prefix. 23 | */ 24 | final class Ipv4 { 25 | private final String str; 26 | private int index; 27 | private final List octets; 28 | private int prefixLen; 29 | 30 | Ipv4(String str) { 31 | this.str = str; 32 | this.octets = new ArrayList<>(); 33 | } 34 | 35 | /** 36 | * Returns the 32-bit value of an address parsed through address() or addressPrefix(). 37 | * 38 | *

Returns -1 if no address was parsed successfully. 39 | */ 40 | int getBits() { 41 | if (this.octets.size() != 4) { 42 | return -1; 43 | } 44 | return (this.octets.get(0) << 24) 45 | | (this.octets.get(1) << 16) 46 | | (this.octets.get(2) << 8) 47 | | this.octets.get(3); 48 | } 49 | 50 | /** 51 | * Returns true if all bits to the right of the prefix-length are all zeros. 52 | * 53 | *

Behavior is undefined if addressPrefix() has not been called before, or has returned false. 54 | */ 55 | boolean isPrefixOnly() { 56 | int bits = this.getBits(); 57 | 58 | int mask = 0; 59 | if (this.prefixLen == 32) { 60 | mask = 0xffffffff; 61 | } else { 62 | mask = ~(0xffffffff >>> this.prefixLen); 63 | } 64 | 65 | int masked = bits & mask; 66 | 67 | return bits == masked; 68 | } 69 | 70 | // Parses an IPv4 Address in dotted decimal notation. 71 | boolean address() { 72 | return this.addressPart() && this.index == this.str.length(); 73 | } 74 | 75 | // Parses an IPv4 Address prefix. 76 | boolean addressPrefix() { 77 | return this.addressPart() 78 | && this.take('/') 79 | && this.prefixLength() 80 | && this.index == this.str.length(); 81 | } 82 | 83 | // Store value in prefixLen 84 | private boolean prefixLength() { 85 | int start = this.index; 86 | 87 | while (this.index < this.str.length() && this.digit()) { 88 | if (this.index - start > 2) { 89 | // max prefix-length is 32 bits, so anything more than 2 digits is invalid 90 | return false; 91 | } 92 | } 93 | 94 | String str = this.str.substring(start, this.index); 95 | if (str.isEmpty()) { 96 | // too short 97 | return false; 98 | } 99 | 100 | if (str.length() > 1 && str.charAt(0) == '0') { 101 | // bad leading 0 102 | return false; 103 | } 104 | 105 | try { 106 | int val = Integer.parseInt(str); 107 | 108 | if (val > 32) { 109 | // max 32 bits 110 | return false; 111 | } 112 | 113 | this.prefixLen = val; 114 | return true; 115 | } catch (NumberFormatException nfe) { 116 | return false; 117 | } 118 | } 119 | 120 | private boolean addressPart() { 121 | int start = this.index; 122 | 123 | if (this.decOctet() 124 | && this.take('.') 125 | && this.decOctet() 126 | && this.take('.') 127 | && this.decOctet() 128 | && this.take('.') 129 | && this.decOctet()) { 130 | return true; 131 | } 132 | 133 | this.index = start; 134 | 135 | return false; 136 | } 137 | 138 | private boolean decOctet() { 139 | int start = this.index; 140 | 141 | while (this.index < this.str.length() && this.digit()) { 142 | if (this.index - start > 3) { 143 | // decimal octet can be three characters at most 144 | return false; 145 | } 146 | } 147 | 148 | String str = this.str.substring(start, this.index); 149 | if (str.isEmpty()) { 150 | // too short 151 | return false; 152 | } 153 | 154 | if (str.length() > 1 && str.charAt(0) == '0') { 155 | // bad leading 0 156 | return false; 157 | } 158 | 159 | try { 160 | int val = Integer.parseInt(str); 161 | 162 | if (val > 255) { 163 | return false; 164 | } 165 | 166 | this.octets.add((short) val); 167 | 168 | return true; 169 | } catch (NumberFormatException nfe) { 170 | // Error converting to number 171 | return false; 172 | } 173 | } 174 | 175 | /** 176 | * Determines whether the current position is a digit. 177 | * 178 | *

Parses the rule: 179 | * 180 | *

DIGIT = %x30-39 ; 0-9
181 |    */
182 |   private boolean digit() {
183 |     char c = this.str.charAt(this.index);
184 |     if ('0' <= c && c <= '9') {
185 |       this.index++;
186 |       return true;
187 |     }
188 |     return false;
189 |   }
190 | 
191 |   /** Take the given char at the current position, incrementing the index if necessary. */
192 |   private boolean take(char c) {
193 |     if (this.index >= this.str.length()) {
194 |       return false;
195 |     }
196 | 
197 |     if (this.str.charAt(this.index) == c) {
198 |       this.index++;
199 |       return true;
200 |     }
201 | 
202 |     return false;
203 |   }
204 | }
205 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ListElementValue.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import com.google.protobuf.Descriptors;
18 | import com.google.protobuf.Message;
19 | import java.util.Collections;
20 | import java.util.List;
21 | import java.util.Map;
22 | import org.jspecify.annotations.Nullable;
23 | 
24 | /**
25 |  * The {@link Value} type that contains a field descriptor for repeated field and the value of an
26 |  * element.
27 |  */
28 | final class ListElementValue implements Value {
29 |   /** Object type since the object type is inferred from the field descriptor. */
30 |   private final Object value;
31 | 
32 |   /**
33 |    * {@link com.google.protobuf.Descriptors.FieldDescriptor} is the field descriptor for the value.
34 |    */
35 |   private final Descriptors.FieldDescriptor fieldDescriptor;
36 | 
37 |   ListElementValue(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
38 |     this.value = value;
39 |     this.fieldDescriptor = fieldDescriptor;
40 |   }
41 | 
42 |   @Override
43 |   public Descriptors.@Nullable FieldDescriptor fieldDescriptor() {
44 |     return fieldDescriptor;
45 |   }
46 | 
47 |   @Override
48 |   public @Nullable Message messageValue() {
49 |     if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
50 |       return (Message) value;
51 |     }
52 |     return null;
53 |   }
54 | 
55 |   @Override
56 |   public  T value(Class clazz) {
57 |     Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType();
58 |     if (type == Descriptors.FieldDescriptor.Type.MESSAGE) {
59 |       return clazz.cast(value);
60 |     }
61 |     return clazz.cast(ProtoAdapter.scalarToCel(type, value));
62 |   }
63 | 
64 |   @Override
65 |   public List repeatedValue() {
66 |     return Collections.emptyList();
67 |   }
68 | 
69 |   @Override
70 |   public Map mapValue() {
71 |     return Collections.emptyMap();
72 |   }
73 | }
74 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ListEvaluator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.protovalidate.exceptions.ExecutionException;
18 | import build.buf.validate.FieldPath;
19 | import build.buf.validate.FieldPathElement;
20 | import build.buf.validate.FieldRules;
21 | import build.buf.validate.RepeatedRules;
22 | import java.util.ArrayList;
23 | import java.util.List;
24 | import java.util.Objects;
25 | 
26 | /** Performs validation on the elements of a repeated field. */
27 | final class ListEvaluator implements Evaluator {
28 |   /** Rule path to repeated rules */
29 |   private static final FieldPath REPEATED_ITEMS_RULE_PATH =
30 |       FieldPath.newBuilder()
31 |           .addElements(
32 |               FieldPathUtils.fieldPathElement(
33 |                   FieldRules.getDescriptor().findFieldByNumber(FieldRules.REPEATED_FIELD_NUMBER)))
34 |           .addElements(
35 |               FieldPathUtils.fieldPathElement(
36 |                   RepeatedRules.getDescriptor()
37 |                       .findFieldByNumber(RepeatedRules.ITEMS_FIELD_NUMBER)))
38 |           .build();
39 | 
40 |   private final RuleViolationHelper helper;
41 | 
42 |   /** Rules are checked on every item of the list. */
43 |   final ValueEvaluator itemRules;
44 | 
45 |   /** Constructs a {@link ListEvaluator}. */
46 |   ListEvaluator(ValueEvaluator valueEvaluator) {
47 |     this.helper = new RuleViolationHelper(valueEvaluator);
48 |     this.itemRules = new ValueEvaluator(null, REPEATED_ITEMS_RULE_PATH);
49 |   }
50 | 
51 |   @Override
52 |   public boolean tautology() {
53 |     return itemRules.tautology();
54 |   }
55 | 
56 |   @Override
57 |   public List evaluate(Value val, boolean failFast)
58 |       throws ExecutionException {
59 |     List allViolations = new ArrayList<>();
60 |     List repeatedValues = val.repeatedValue();
61 |     for (int i = 0; i < repeatedValues.size(); i++) {
62 |       List violations = itemRules.evaluate(repeatedValues.get(i), failFast);
63 |       if (violations.isEmpty()) {
64 |         continue;
65 |       }
66 |       FieldPathElement fieldPathElement =
67 |           Objects.requireNonNull(helper.getFieldPathElement()).toBuilder().setIndex(i).build();
68 |       FieldPathUtils.updatePaths(violations, fieldPathElement, helper.getRulePrefixElements());
69 |       if (failFast && !violations.isEmpty()) {
70 |         return violations;
71 |       }
72 |       allViolations.addAll(violations);
73 |     }
74 |     return allViolations;
75 |   }
76 | }
77 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/MapEvaluator.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2023-2025 Buf Technologies, Inc.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //      http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package build.buf.protovalidate;
 16 | 
 17 | import build.buf.protovalidate.exceptions.ExecutionException;
 18 | import build.buf.validate.FieldPath;
 19 | import build.buf.validate.FieldPathElement;
 20 | import build.buf.validate.FieldRules;
 21 | import build.buf.validate.MapRules;
 22 | import com.google.protobuf.Descriptors;
 23 | import java.util.ArrayList;
 24 | import java.util.Collections;
 25 | import java.util.List;
 26 | import java.util.Map;
 27 | import java.util.Objects;
 28 | import java.util.stream.Collectors;
 29 | 
 30 | /** Performs validation on a map field's key-value pairs. */
 31 | final class MapEvaluator implements Evaluator {
 32 |   /** Rule path to map key rules */
 33 |   private static final FieldPath MAP_KEYS_RULE_PATH =
 34 |       FieldPath.newBuilder()
 35 |           .addElements(
 36 |               FieldPathUtils.fieldPathElement(
 37 |                   FieldRules.getDescriptor().findFieldByNumber(FieldRules.MAP_FIELD_NUMBER)))
 38 |           .addElements(
 39 |               FieldPathUtils.fieldPathElement(
 40 |                   MapRules.getDescriptor().findFieldByNumber(MapRules.KEYS_FIELD_NUMBER)))
 41 |           .build();
 42 | 
 43 |   /** Rule path to map value rules */
 44 |   private static final FieldPath MAP_VALUES_RULE_PATH =
 45 |       FieldPath.newBuilder()
 46 |           .addElements(
 47 |               FieldPathUtils.fieldPathElement(
 48 |                   FieldRules.getDescriptor().findFieldByNumber(FieldRules.MAP_FIELD_NUMBER)))
 49 |           .addElements(
 50 |               FieldPathUtils.fieldPathElement(
 51 |                   MapRules.getDescriptor().findFieldByNumber(MapRules.VALUES_FIELD_NUMBER)))
 52 |           .build();
 53 | 
 54 |   private final RuleViolationHelper helper;
 55 | 
 56 |   /** Rule for checking the map keys */
 57 |   private final ValueEvaluator keyEvaluator;
 58 | 
 59 |   /** Rule for checking the map values */
 60 |   private final ValueEvaluator valueEvaluator;
 61 | 
 62 |   /** Field descriptor of the map field */
 63 |   final Descriptors.FieldDescriptor fieldDescriptor;
 64 | 
 65 |   /** Field descriptor of the map key field */
 66 |   final Descriptors.FieldDescriptor keyFieldDescriptor;
 67 | 
 68 |   /** Field descriptor of the map value field */
 69 |   final Descriptors.FieldDescriptor valueFieldDescriptor;
 70 | 
 71 |   /**
 72 |    * Constructs a {@link MapEvaluator}.
 73 |    *
 74 |    * @param valueEvaluator The value evaluator this rule exists under.
 75 |    */
 76 |   MapEvaluator(ValueEvaluator valueEvaluator, Descriptors.FieldDescriptor fieldDescriptor) {
 77 |     this.helper = new RuleViolationHelper(valueEvaluator);
 78 |     this.keyEvaluator = new ValueEvaluator(null, MAP_KEYS_RULE_PATH);
 79 |     this.valueEvaluator = new ValueEvaluator(null, MAP_VALUES_RULE_PATH);
 80 |     this.fieldDescriptor = fieldDescriptor;
 81 |     this.keyFieldDescriptor = fieldDescriptor.getMessageType().findFieldByNumber(1);
 82 |     this.valueFieldDescriptor = fieldDescriptor.getMessageType().findFieldByNumber(2);
 83 |   }
 84 | 
 85 |   /**
 86 |    * Gets the key evaluator associated with this map evaluator.
 87 |    *
 88 |    * @return The key evaluator.
 89 |    */
 90 |   ValueEvaluator getKeyEvaluator() {
 91 |     return keyEvaluator;
 92 |   }
 93 | 
 94 |   /**
 95 |    * Gets the value evaluator associated with this map evaluator.
 96 |    *
 97 |    * @return The value evaluator.
 98 |    */
 99 |   ValueEvaluator getValueEvaluator() {
100 |     return valueEvaluator;
101 |   }
102 | 
103 |   @Override
104 |   public boolean tautology() {
105 |     return keyEvaluator.tautology() && valueEvaluator.tautology();
106 |   }
107 | 
108 |   @Override
109 |   public List evaluate(Value val, boolean failFast)
110 |       throws ExecutionException {
111 |     List violations = new ArrayList<>();
112 |     Map mapValue = val.mapValue();
113 |     for (Map.Entry entry : mapValue.entrySet()) {
114 |       violations.addAll(evalPairs(entry.getKey(), entry.getValue(), failFast));
115 |       if (failFast && !violations.isEmpty()) {
116 |         return violations;
117 |       }
118 |     }
119 |     if (violations.isEmpty()) {
120 |       return RuleViolation.NO_VIOLATIONS;
121 |     }
122 |     return violations;
123 |   }
124 | 
125 |   private List evalPairs(Value key, Value value, boolean failFast)
126 |       throws ExecutionException {
127 |     List keyViolations =
128 |         keyEvaluator.evaluate(key, failFast).stream()
129 |             .map(violation -> violation.setForKey(true))
130 |             .collect(Collectors.toList());
131 |     final List valueViolations;
132 |     if (failFast && !keyViolations.isEmpty()) {
133 |       // Don't evaluate value rules if failFast is enabled and keys failed validation.
134 |       // We still need to continue execution to the end to properly prefix violation field paths.
135 |       valueViolations = RuleViolation.NO_VIOLATIONS;
136 |     } else {
137 |       valueViolations = valueEvaluator.evaluate(value, failFast);
138 |     }
139 |     if (keyViolations.isEmpty() && valueViolations.isEmpty()) {
140 |       return Collections.emptyList();
141 |     }
142 |     List violations =
143 |         new ArrayList<>(keyViolations.size() + valueViolations.size());
144 |     violations.addAll(keyViolations);
145 |     violations.addAll(valueViolations);
146 | 
147 |     FieldPathElement.Builder fieldPathElementBuilder =
148 |         Objects.requireNonNull(helper.getFieldPathElement()).toBuilder();
149 |     fieldPathElementBuilder.setKeyType(keyFieldDescriptor.getType().toProto());
150 |     fieldPathElementBuilder.setValueType(valueFieldDescriptor.getType().toProto());
151 |     switch (keyFieldDescriptor.getType().toProto()) {
152 |       case TYPE_INT64:
153 |       case TYPE_INT32:
154 |       case TYPE_SINT32:
155 |       case TYPE_SINT64:
156 |       case TYPE_SFIXED32:
157 |       case TYPE_SFIXED64:
158 |         fieldPathElementBuilder.setIntKey(key.value(Number.class).longValue());
159 |         break;
160 |       case TYPE_UINT32:
161 |       case TYPE_UINT64:
162 |       case TYPE_FIXED32:
163 |       case TYPE_FIXED64:
164 |         fieldPathElementBuilder.setUintKey(key.value(Number.class).longValue());
165 |         break;
166 |       case TYPE_BOOL:
167 |         fieldPathElementBuilder.setBoolKey(key.value(Boolean.class));
168 |         break;
169 |       case TYPE_STRING:
170 |         fieldPathElementBuilder.setStringKey(key.value(String.class));
171 |         break;
172 |       default:
173 |         throw new ExecutionException("Unexpected map key type");
174 |     }
175 |     FieldPathElement fieldPathElement = fieldPathElementBuilder.build();
176 |     return FieldPathUtils.updatePaths(violations, fieldPathElement, helper.getRulePrefixElements());
177 |   }
178 | }
179 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/MessageEvaluator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.protovalidate.exceptions.ExecutionException;
18 | import java.util.ArrayList;
19 | import java.util.List;
20 | 
21 | /** Performs validation on a {@link com.google.protobuf.Message}. */
22 | final class MessageEvaluator implements Evaluator {
23 |   /** List of {@link Evaluator}s that are applied to a message. */
24 |   private final List evaluators = new ArrayList<>();
25 | 
26 |   @Override
27 |   public boolean tautology() {
28 |     for (Evaluator evaluator : evaluators) {
29 |       if (!evaluator.tautology()) {
30 |         return false;
31 |       }
32 |     }
33 |     return true;
34 |   }
35 | 
36 |   @Override
37 |   public List evaluate(Value val, boolean failFast)
38 |       throws ExecutionException {
39 |     List allViolations = new ArrayList<>();
40 |     for (Evaluator evaluator : evaluators) {
41 |       List violations = evaluator.evaluate(val, failFast);
42 |       if (failFast && !violations.isEmpty()) {
43 |         return violations;
44 |       }
45 |       allViolations.addAll(violations);
46 |     }
47 |     if (allViolations.isEmpty()) {
48 |       return RuleViolation.NO_VIOLATIONS;
49 |     }
50 |     return allViolations;
51 |   }
52 | 
53 |   /**
54 |    * Appends an {@link Evaluator} to the list of evaluators.
55 |    *
56 |    * @param eval The evaluator to append.
57 |    */
58 |   void append(Evaluator eval) {
59 |     evaluators.add(eval);
60 |   }
61 | }
62 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/MessageOneofEvaluator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.protovalidate.exceptions.ExecutionException;
18 | import com.google.protobuf.Descriptors.FieldDescriptor;
19 | import com.google.protobuf.Message;
20 | import java.util.Collections;
21 | import java.util.List;
22 | import java.util.stream.Collectors;
23 | 
24 | /**
25 |  * A specialized {@link Evaluator} for applying {@code buf.validate.MessageOneofRule} to a {@link
26 |  * com.google.protobuf.Message}.
27 |  */
28 | final class MessageOneofEvaluator implements Evaluator {
29 |   /** List of fields that are part of the oneof */
30 |   final List fields;
31 | 
32 |   /** If at least one must be set. */
33 |   final boolean required;
34 | 
35 |   MessageOneofEvaluator(List fields, boolean required) {
36 |     this.fields = fields;
37 |     this.required = required;
38 |   }
39 | 
40 |   @Override
41 |   public boolean tautology() {
42 |     return false;
43 |   }
44 | 
45 |   @Override
46 |   public List evaluate(Value val, boolean failFast)
47 |       throws ExecutionException {
48 |     Message msg = val.messageValue();
49 |     if (msg == null) {
50 |       return RuleViolation.NO_VIOLATIONS;
51 |     }
52 |     int hasCount = 0;
53 |     for (FieldDescriptor field : fields) {
54 |       if (msg.hasField(field)) {
55 |         hasCount++;
56 |       }
57 |     }
58 |     if (hasCount > 1) {
59 |       return Collections.singletonList(
60 |           RuleViolation.newBuilder()
61 |               .setRuleId("message.oneof")
62 |               .setMessage(String.format("only one of %s can be set", fieldNames())));
63 |     }
64 |     if (this.required && hasCount == 0) {
65 |       return Collections.singletonList(
66 |           RuleViolation.newBuilder()
67 |               .setRuleId("message.oneof")
68 |               .setMessage(String.format("one of %s must be set", fieldNames())));
69 |     }
70 |     return Collections.emptyList();
71 |   }
72 | 
73 |   String fieldNames() {
74 |     return fields.stream().map(FieldDescriptor::getName).collect(Collectors.joining(", "));
75 |   }
76 | }
77 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/MessageValue.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import com.google.protobuf.Descriptors;
18 | import com.google.protobuf.Message;
19 | import java.util.Collections;
20 | import java.util.List;
21 | import java.util.Map;
22 | import org.jspecify.annotations.Nullable;
23 | 
24 | /** The {@link Value} type that contains a {@link com.google.protobuf.Message}. */
25 | final class MessageValue implements Value {
26 | 
27 |   /** Object type since the object type is inferred from the field descriptor. */
28 |   private final Object value;
29 | 
30 |   /**
31 |    * Constructs a {@link MessageValue} with the provided message value.
32 |    *
33 |    * @param value The message value.
34 |    */
35 |   MessageValue(Message value) {
36 |     this.value = value;
37 |   }
38 | 
39 |   @Override
40 |   public Descriptors.@Nullable FieldDescriptor fieldDescriptor() {
41 |     return null;
42 |   }
43 | 
44 |   @Override
45 |   public Message messageValue() {
46 |     return (Message) value;
47 |   }
48 | 
49 |   @Override
50 |   public  T value(Class clazz) {
51 |     return clazz.cast(value);
52 |   }
53 | 
54 |   @Override
55 |   public List repeatedValue() {
56 |     return Collections.emptyList();
57 |   }
58 | 
59 |   @Override
60 |   public Map mapValue() {
61 |     return Collections.emptyMap();
62 |   }
63 | }
64 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/NowVariable.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import com.google.protobuf.Timestamp;
18 | import dev.cel.runtime.CelVariableResolver;
19 | import java.time.Instant;
20 | import java.util.Optional;
21 | import org.jspecify.annotations.Nullable;
22 | 
23 | /**
24 |  * {@link NowVariable} implements {@link CelVariableResolver}, providing a lazily produced timestamp
25 |  * for accessing the variable `now` that's constant within an evaluation.
26 |  */
27 | final class NowVariable implements CelVariableResolver {
28 |   /** The name of the 'now' variable. */
29 |   static final String NOW_NAME = "now";
30 | 
31 |   /** The resolved value of the 'now' variable. */
32 |   @Nullable private Timestamp now;
33 | 
34 |   /** Creates an instance of a "now" variable. */
35 |   NowVariable() {}
36 | 
37 |   @Override
38 |   public Optional find(String name) {
39 |     if (!name.equals(NOW_NAME)) {
40 |       return Optional.empty();
41 |     }
42 |     if (this.now == null) {
43 |       Instant nowInstant = Instant.now();
44 |       now =
45 |           Timestamp.newBuilder()
46 |               .setSeconds(nowInstant.getEpochSecond())
47 |               .setNanos(nowInstant.getNano())
48 |               .build();
49 |     }
50 |     return Optional.of(this.now);
51 |   }
52 | }
53 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ObjectValue.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2023-2025 Buf Technologies, Inc.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //      http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package build.buf.protovalidate;
 16 | 
 17 | import com.google.protobuf.AbstractMessage;
 18 | import com.google.protobuf.Descriptors;
 19 | import com.google.protobuf.Message;
 20 | import java.util.ArrayList;
 21 | import java.util.Collections;
 22 | import java.util.HashMap;
 23 | import java.util.List;
 24 | import java.util.Map;
 25 | import org.jspecify.annotations.Nullable;
 26 | 
 27 | /** The {@link Value} type that contains a field descriptor and its value. */
 28 | final class ObjectValue implements Value {
 29 | 
 30 |   /**
 31 |    * {@link com.google.protobuf.Descriptors.FieldDescriptor} is the field descriptor for the value.
 32 |    */
 33 |   private final Descriptors.FieldDescriptor fieldDescriptor;
 34 | 
 35 |   /** Object type since the object type is inferred from the field descriptor. */
 36 |   private final Object value;
 37 | 
 38 |   /**
 39 |    * Constructs a new {@link ObjectValue}.
 40 |    *
 41 |    * @param fieldDescriptor The field descriptor for the value.
 42 |    * @param value The value associated with the field descriptor.
 43 |    */
 44 |   ObjectValue(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
 45 |     this.fieldDescriptor = fieldDescriptor;
 46 |     this.value = value;
 47 |   }
 48 | 
 49 |   @Override
 50 |   public Descriptors.FieldDescriptor fieldDescriptor() {
 51 |     return fieldDescriptor;
 52 |   }
 53 | 
 54 |   @Nullable
 55 |   @Override
 56 |   public Message messageValue() {
 57 |     if (fieldDescriptor.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE) {
 58 |       return (Message) value;
 59 |     }
 60 |     return null;
 61 |   }
 62 | 
 63 |   @Override
 64 |   public  T value(Class clazz) {
 65 |     return clazz.cast(ProtoAdapter.toCel(fieldDescriptor, value));
 66 |   }
 67 | 
 68 |   @Override
 69 |   public List repeatedValue() {
 70 |     List out = new ArrayList<>();
 71 |     if (fieldDescriptor.isRepeated()) {
 72 |       List list = (List) value;
 73 |       for (Object o : list) {
 74 |         out.add(new ListElementValue(fieldDescriptor, o));
 75 |       }
 76 |     }
 77 |     return out;
 78 |   }
 79 | 
 80 |   @Override
 81 |   public Map mapValue() {
 82 |     List input =
 83 |         value instanceof List
 84 |             ? (List) value
 85 |             : Collections.singletonList((AbstractMessage) value);
 86 | 
 87 |     Descriptors.FieldDescriptor keyDesc = fieldDescriptor.getMessageType().findFieldByNumber(1);
 88 |     Descriptors.FieldDescriptor valDesc = fieldDescriptor.getMessageType().findFieldByNumber(2);
 89 |     Map out = new HashMap<>(input.size());
 90 |     for (AbstractMessage entry : input) {
 91 |       Object keyValue = entry.getField(keyDesc);
 92 |       Value keyJavaValue = new ObjectValue(keyDesc, keyValue);
 93 | 
 94 |       Object valValue = entry.getField(valDesc);
 95 |       Value valJavaValue = new ObjectValue(valDesc, valValue);
 96 | 
 97 |       out.put(keyJavaValue, valJavaValue);
 98 |     }
 99 | 
100 |     return out;
101 |   }
102 | }
103 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/OneofEvaluator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.protovalidate.exceptions.ExecutionException;
18 | import build.buf.validate.FieldPathElement;
19 | import com.google.protobuf.Descriptors.OneofDescriptor;
20 | import com.google.protobuf.Message;
21 | import java.util.Collections;
22 | import java.util.List;
23 | 
24 | /** {@link OneofEvaluator} performs validation on a oneof union. */
25 | final class OneofEvaluator implements Evaluator {
26 |   /** The {@link OneofDescriptor} targeted by this evaluator. */
27 |   private final OneofDescriptor descriptor;
28 | 
29 |   /** Indicates that a member of the oneof must be set. */
30 |   private final boolean required;
31 | 
32 |   /**
33 |    * Constructs a {@link OneofEvaluator}.
34 |    *
35 |    * @param descriptor The targeted oneof descriptor.
36 |    * @param required Indicates whether a member of the oneof must be set.
37 |    */
38 |   OneofEvaluator(OneofDescriptor descriptor, boolean required) {
39 |     this.descriptor = descriptor;
40 |     this.required = required;
41 |   }
42 | 
43 |   @Override
44 |   public boolean tautology() {
45 |     return !required;
46 |   }
47 | 
48 |   @Override
49 |   public List evaluate(Value val, boolean failFast)
50 |       throws ExecutionException {
51 |     Message message = val.messageValue();
52 |     if (message == null || !required || (message.getOneofFieldDescriptor(descriptor) != null)) {
53 |       return RuleViolation.NO_VIOLATIONS;
54 |     }
55 |     return Collections.singletonList(
56 |         RuleViolation.newBuilder()
57 |             .addFirstFieldPathElement(
58 |                 FieldPathElement.newBuilder().setFieldName(descriptor.getName()).build())
59 |             .setRuleId("required")
60 |             .setMessage("exactly one field is required in oneof"));
61 |   }
62 | }
63 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ProtoAdapter.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2023-2025 Buf Technologies, Inc.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //      http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package build.buf.protovalidate;
 16 | 
 17 | import com.google.common.primitives.UnsignedLong;
 18 | import com.google.protobuf.AbstractMessage;
 19 | import com.google.protobuf.ByteString;
 20 | import com.google.protobuf.Descriptors;
 21 | import com.google.protobuf.Message;
 22 | import com.google.protobuf.Timestamp;
 23 | import dev.cel.common.values.CelByteString;
 24 | import java.time.Duration;
 25 | import java.time.Instant;
 26 | import java.util.ArrayList;
 27 | import java.util.Collections;
 28 | import java.util.HashMap;
 29 | import java.util.List;
 30 | import java.util.Map;
 31 | 
 32 | /**
 33 |  * CEL supports protobuf natively but when we pass it field values (like scalars, repeated, and
 34 |  * maps) it has no way to treat them like a proto message field. This class has methods to convert
 35 |  * to a cel values.
 36 |  */
 37 | final class ProtoAdapter {
 38 |   /** Converts a protobuf field value to CEL compatible value. */
 39 |   static Object toCel(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
 40 |     Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType();
 41 |     if (fieldDescriptor.isMapField()) {
 42 |       List input =
 43 |           value instanceof List
 44 |               ? (List) value
 45 |               : Collections.singletonList((AbstractMessage) value);
 46 |       Descriptors.FieldDescriptor keyDesc = fieldDescriptor.getMessageType().findFieldByNumber(1);
 47 |       Descriptors.FieldDescriptor valDesc = fieldDescriptor.getMessageType().findFieldByNumber(2);
 48 |       Map out = new HashMap<>(input.size());
 49 | 
 50 |       for (AbstractMessage entry : input) {
 51 |         Object keyValue = entry.getField(keyDesc);
 52 |         Object valValue = entry.getField(valDesc);
 53 |         out.put(toCel(keyDesc, keyValue), toCel(valDesc, valValue));
 54 |       }
 55 |       return out;
 56 |     }
 57 |     if (fieldDescriptor.isRepeated()) {
 58 |       List list = (List) value;
 59 |       List out = new ArrayList<>(list.size());
 60 |       for (Object element : list) {
 61 |         out.add(scalarToCel(type, element));
 62 |       }
 63 |       return out;
 64 |     }
 65 |     return scalarToCel(type, value);
 66 |   }
 67 | 
 68 |   /** Converts a scalar type to cel value. */
 69 |   static Object scalarToCel(Descriptors.FieldDescriptor.Type type, Object value) {
 70 |     switch (type) {
 71 |       case BYTES:
 72 |         if (value instanceof ByteString) {
 73 |           return CelByteString.of(((ByteString) value).toByteArray());
 74 |         }
 75 |         return value;
 76 |       case ENUM:
 77 |         if (value instanceof Descriptors.EnumValueDescriptor) {
 78 |           return (long) ((Descriptors.EnumValueDescriptor) value).getNumber();
 79 |         }
 80 |         return value;
 81 |       case FLOAT:
 82 |         return Double.valueOf((Float) value);
 83 |       case INT32:
 84 |       case SINT32:
 85 |       case SFIXED32:
 86 |         return Long.valueOf((Integer) value);
 87 |       case FIXED32:
 88 |       case UINT32:
 89 |         return UnsignedLong.fromLongBits(Long.valueOf((Integer) value));
 90 |       case UINT64:
 91 |       case FIXED64:
 92 |         return UnsignedLong.fromLongBits((Long) value);
 93 |       case MESSAGE:
 94 |         // cel-java 0.11.1 added support for java.time.Instant and java.time.Duration.
 95 |         Message msg = (Message) value;
 96 |         if (msg instanceof com.google.protobuf.Timestamp) {
 97 |           Timestamp timestamp = (Timestamp) value;
 98 |           return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
 99 |         }
100 |         if (msg instanceof com.google.protobuf.Duration) {
101 |           com.google.protobuf.Duration duration = (com.google.protobuf.Duration) value;
102 |           return Duration.ofSeconds(duration.getSeconds(), duration.getNanos());
103 |         }
104 |         return value;
105 |       default:
106 |         return value;
107 |     }
108 |   }
109 | }
110 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/RuleResolver.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2023-2025 Buf Technologies, Inc.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //      http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package build.buf.protovalidate;
 16 | 
 17 | import build.buf.protovalidate.exceptions.CompilationException;
 18 | import build.buf.validate.FieldRules;
 19 | import build.buf.validate.MessageRules;
 20 | import build.buf.validate.OneofRules;
 21 | import build.buf.validate.ValidateProto;
 22 | import com.google.protobuf.DescriptorProtos;
 23 | import com.google.protobuf.Descriptors.Descriptor;
 24 | import com.google.protobuf.Descriptors.FieldDescriptor;
 25 | import com.google.protobuf.Descriptors.OneofDescriptor;
 26 | import com.google.protobuf.ExtensionRegistry;
 27 | import com.google.protobuf.InvalidProtocolBufferException;
 28 | import com.google.protobuf.MessageLite;
 29 | 
 30 | /** Manages the resolution of protovalidate rules. */
 31 | final class RuleResolver {
 32 |   private static final ExtensionRegistry EXTENSION_REGISTRY = ExtensionRegistry.newInstance();
 33 | 
 34 |   static {
 35 |     EXTENSION_REGISTRY.add(ValidateProto.message);
 36 |     EXTENSION_REGISTRY.add(ValidateProto.oneof);
 37 |     EXTENSION_REGISTRY.add(ValidateProto.field);
 38 |   }
 39 | 
 40 |   /**
 41 |    * Resolves the rules for a message descriptor.
 42 |    *
 43 |    * @param desc the message descriptor.
 44 |    * @return the resolved {@link MessageRules}.
 45 |    */
 46 |   MessageRules resolveMessageRules(Descriptor desc)
 47 |       throws InvalidProtocolBufferException, CompilationException {
 48 |     DescriptorProtos.MessageOptions options = desc.getOptions();
 49 |     // If the protovalidate message extension is unknown, reparse using extension registry.
 50 |     if (options.getUnknownFields().hasField(ValidateProto.message.getNumber())) {
 51 |       options =
 52 |           DescriptorProtos.MessageOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY);
 53 |     }
 54 |     if (!options.hasExtension(ValidateProto.message)) {
 55 |       return MessageRules.getDefaultInstance();
 56 |     }
 57 |     // Don't use getExtension here to avoid exception if descriptor types don't match.
 58 |     // This can occur if the extension is generated to a different Java package.
 59 |     Object value = options.getField(ValidateProto.message.getDescriptor());
 60 |     if (value instanceof MessageRules) {
 61 |       return ((MessageRules) value);
 62 |     }
 63 |     if (value instanceof MessageLite) {
 64 |       // Possible that this represents the same rule type, just generated to a different
 65 |       // java_package.
 66 |       return MessageRules.parseFrom(((MessageLite) value).toByteString());
 67 |     }
 68 |     throw new CompilationException("unexpected message rule option type: " + value);
 69 |   }
 70 | 
 71 |   /**
 72 |    * Resolves the rules for a oneof descriptor.
 73 |    *
 74 |    * @param desc the oneof descriptor.
 75 |    * @return the resolved {@link OneofRules}.
 76 |    */
 77 |   OneofRules resolveOneofRules(OneofDescriptor desc)
 78 |       throws InvalidProtocolBufferException, CompilationException {
 79 |     DescriptorProtos.OneofOptions options = desc.getOptions();
 80 |     // If the protovalidate oneof extension is unknown, reparse using extension registry.
 81 |     if (options.getUnknownFields().hasField(ValidateProto.oneof.getNumber())) {
 82 |       options = DescriptorProtos.OneofOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY);
 83 |     }
 84 |     if (!options.hasExtension(ValidateProto.oneof)) {
 85 |       return OneofRules.getDefaultInstance();
 86 |     }
 87 |     // Don't use getExtension here to avoid exception if descriptor types don't match.
 88 |     // This can occur if the extension is generated to a different Java package.
 89 |     Object value = options.getField(ValidateProto.oneof.getDescriptor());
 90 |     if (value instanceof OneofRules) {
 91 |       return ((OneofRules) value);
 92 |     }
 93 |     if (value instanceof MessageLite) {
 94 |       // Possible that this represents the same rule type, just generated to a different
 95 |       // java_package.
 96 |       return OneofRules.parseFrom(((MessageLite) value).toByteString());
 97 |     }
 98 |     throw new CompilationException("unexpected oneof rule option type: " + value);
 99 |   }
100 | 
101 |   /**
102 |    * Resolves the rules for a field descriptor.
103 |    *
104 |    * @param desc the field descriptor.
105 |    * @return the resolved {@link FieldRules}.
106 |    */
107 |   FieldRules resolveFieldRules(FieldDescriptor desc)
108 |       throws InvalidProtocolBufferException, CompilationException {
109 |     DescriptorProtos.FieldOptions options = desc.getOptions();
110 |     // If the protovalidate field option is unknown, reparse using extension registry.
111 |     if (options.getUnknownFields().hasField(ValidateProto.field.getNumber())) {
112 |       options = DescriptorProtos.FieldOptions.parseFrom(options.toByteString(), EXTENSION_REGISTRY);
113 |     }
114 |     if (!options.hasExtension(ValidateProto.field)) {
115 |       return FieldRules.getDefaultInstance();
116 |     }
117 |     // Don't use getExtension here to avoid exception if descriptor types don't match.
118 |     // This can occur if the extension is generated to a different Java package.
119 |     Object value = options.getField(ValidateProto.field.getDescriptor());
120 |     if (value instanceof FieldRules) {
121 |       return ((FieldRules) value);
122 |     }
123 |     if (value instanceof MessageLite) {
124 |       // Possible that this represents the same rule type, just generated to a different
125 |       // java_package.
126 |       return FieldRules.parseFrom(((MessageLite) value).toByteString());
127 |     }
128 |     throw new CompilationException("unexpected field rule option type: " + value);
129 |   }
130 | }
131 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/RuleViolationHelper.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.validate.FieldPath;
18 | import build.buf.validate.FieldPathElement;
19 | import java.util.Collections;
20 | import java.util.List;
21 | import org.jspecify.annotations.Nullable;
22 | 
23 | final class RuleViolationHelper {
24 |   private static final List EMPTY_PREFIX = Collections.emptyList();
25 | 
26 |   private final @Nullable FieldPath rulePrefix;
27 | 
28 |   private final @Nullable FieldPathElement fieldPathElement;
29 | 
30 |   RuleViolationHelper(@Nullable ValueEvaluator evaluator) {
31 |     if (evaluator != null) {
32 |       this.rulePrefix = evaluator.getNestedRule();
33 |       if (evaluator.getDescriptor() != null) {
34 |         this.fieldPathElement = FieldPathUtils.fieldPathElement(evaluator.getDescriptor());
35 |       } else {
36 |         this.fieldPathElement = null;
37 |       }
38 |     } else {
39 |       this.rulePrefix = null;
40 |       this.fieldPathElement = null;
41 |     }
42 |   }
43 | 
44 |   @Nullable FieldPathElement getFieldPathElement() {
45 |     return fieldPathElement;
46 |   }
47 | 
48 |   List getRulePrefixElements() {
49 |     if (rulePrefix == null) {
50 |       return EMPTY_PREFIX;
51 |     }
52 |     return rulePrefix.getElementsList();
53 |   }
54 | }
55 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/UnknownDescriptorEvaluator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.protovalidate.exceptions.ExecutionException;
18 | import com.google.protobuf.Descriptors.Descriptor;
19 | import java.util.Collections;
20 | import java.util.List;
21 | 
22 | /**
23 |  * An {@link Evaluator} for an unknown descriptor. This is returned only if lazy-building of
24 |  * evaluators has been disabled and an unknown descriptor is encountered.
25 |  */
26 | final class UnknownDescriptorEvaluator implements Evaluator {
27 |   /** The descriptor targeted by this evaluator. */
28 |   private final Descriptor desc;
29 | 
30 |   /** Constructs a new {@link UnknownDescriptorEvaluator}. */
31 |   UnknownDescriptorEvaluator(Descriptor desc) {
32 |     this.desc = desc;
33 |   }
34 | 
35 |   @Override
36 |   public boolean tautology() {
37 |     return false;
38 |   }
39 | 
40 |   @Override
41 |   public List evaluate(Value val, boolean failFast)
42 |       throws ExecutionException {
43 |     return Collections.singletonList(
44 |         RuleViolation.newBuilder().setMessage("No evaluator available for " + desc.getFullName()));
45 |   }
46 | }
47 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ValidateLibrary.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import dev.cel.checker.CelCheckerBuilder;
18 | import dev.cel.common.CelVarDecl;
19 | import dev.cel.common.types.SimpleType;
20 | import dev.cel.compiler.CelCompilerLibrary;
21 | import dev.cel.parser.CelParserBuilder;
22 | import dev.cel.parser.CelStandardMacro;
23 | import dev.cel.runtime.CelRuntimeBuilder;
24 | import dev.cel.runtime.CelRuntimeLibrary;
25 | 
26 | /**
27 |  * Custom {@link CelCompilerLibrary} and {@link CelRuntimeLibrary}. Provides all the custom
28 |  * extension function definitions and overloads.
29 |  */
30 | final class ValidateLibrary implements CelCompilerLibrary, CelRuntimeLibrary {
31 | 
32 |   /** Creates a ValidateLibrary with all custom declarations and overloads. */
33 |   ValidateLibrary() {}
34 | 
35 |   @Override
36 |   public void setParserOptions(CelParserBuilder parserBuilder) {
37 |     parserBuilder.setStandardMacros(
38 |         CelStandardMacro.ALL,
39 |         CelStandardMacro.EXISTS,
40 |         CelStandardMacro.EXISTS_ONE,
41 |         CelStandardMacro.FILTER,
42 |         CelStandardMacro.HAS,
43 |         CelStandardMacro.MAP,
44 |         CelStandardMacro.MAP_FILTER);
45 |   }
46 | 
47 |   @Override
48 |   public void setCheckerOptions(CelCheckerBuilder checkerBuilder) {
49 |     checkerBuilder
50 |         .addVarDeclarations(
51 |             CelVarDecl.newVarDeclaration(NowVariable.NOW_NAME, SimpleType.TIMESTAMP))
52 |         .addFunctionDeclarations(CustomDeclarations.create());
53 |   }
54 | 
55 |   @Override
56 |   public void setRuntimeOptions(CelRuntimeBuilder runtimeBuilder) {
57 |     runtimeBuilder.addFunctionBindings(CustomOverload.create());
58 |   }
59 | }
60 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ValidationResult.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2023-2025 Buf Technologies, Inc.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //      http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package build.buf.protovalidate;
 16 | 
 17 | import build.buf.validate.Violations;
 18 | import java.util.ArrayList;
 19 | import java.util.Collections;
 20 | import java.util.List;
 21 | 
 22 | /**
 23 |  * {@link ValidationResult} is returned when a rule is executed. It contains a list of violations.
 24 |  * This is non-fatal. If there are no violations, the rule is considered to have passed.
 25 |  */
 26 | public class ValidationResult {
 27 | 
 28 |   /**
 29 |    * violations is a list of {@link Violation} that occurred during the validations of a message.
 30 |    */
 31 |   private final List violations;
 32 | 
 33 |   /** A violation result with an empty violation list. */
 34 |   public static final ValidationResult EMPTY = new ValidationResult(Collections.emptyList());
 35 | 
 36 |   /**
 37 |    * Creates a violation result from a list of violations.
 38 |    *
 39 |    * @param violations violation list for the result.
 40 |    */
 41 |   public ValidationResult(List violations) {
 42 |     this.violations = violations;
 43 |   }
 44 | 
 45 |   /**
 46 |    * Check if the result is successful.
 47 |    *
 48 |    * @return if the validation result was a success.
 49 |    */
 50 |   public boolean isSuccess() {
 51 |     return violations.isEmpty();
 52 |   }
 53 | 
 54 |   /**
 55 |    * Get the list of violations in the result.
 56 |    *
 57 |    * @return the violation list.
 58 |    */
 59 |   public List getViolations() {
 60 |     return violations;
 61 |   }
 62 | 
 63 |   /**
 64 |    * Returns a string representation of the validation result, including all the violations.
 65 |    *
 66 |    * @return a string representation of the validation result.
 67 |    */
 68 |   @Override
 69 |   public String toString() {
 70 |     StringBuilder builder = new StringBuilder();
 71 |     if (isSuccess()) {
 72 |       builder.append("Validation OK");
 73 |     } else {
 74 |       builder.append("Validation error:");
 75 |       for (Violation violation : violations) {
 76 |         builder.append("\n - ");
 77 |         if (violation.toProto().hasField()) {
 78 |           builder.append(FieldPathUtils.fieldPathString(violation.toProto().getField()));
 79 |           builder.append(": ");
 80 |         }
 81 |         builder.append(
 82 |             String.format(
 83 |                 "%s [%s]", violation.toProto().getMessage(), violation.toProto().getRuleId()));
 84 |       }
 85 |     }
 86 |     return builder.toString();
 87 |   }
 88 | 
 89 |   /**
 90 |    * Converts the validation result to its equivalent protobuf form.
 91 |    *
 92 |    * @return The protobuf form of this validation result.
 93 |    */
 94 |   public build.buf.validate.Violations toProto() {
 95 |     List protoViolations = new ArrayList<>();
 96 |     for (Violation violation : violations) {
 97 |       protoViolations.add(violation.toProto());
 98 |     }
 99 |     return Violations.newBuilder().addAllViolations(protoViolations).build();
100 |   }
101 | }
102 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/Validator.java:
--------------------------------------------------------------------------------
 1 | // Copyright 2023-2025 Buf Technologies, Inc.
 2 | //
 3 | // Licensed under the Apache License, Version 2.0 (the "License");
 4 | // you may not use this file except in compliance with the License.
 5 | // You may obtain a copy of the License at
 6 | //
 7 | //      http://www.apache.org/licenses/LICENSE-2.0
 8 | //
 9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 
15 | package build.buf.protovalidate;
16 | 
17 | import build.buf.protovalidate.exceptions.CompilationException;
18 | import build.buf.protovalidate.exceptions.ExecutionException;
19 | import build.buf.protovalidate.exceptions.ValidationException;
20 | import com.google.protobuf.Message;
21 | 
22 | /** A validator that can be used to validate messages */
23 | public interface Validator {
24 |   /**
25 |    * Checks that message satisfies its rules. Rules are defined within the Protobuf file as options
26 |    * from the buf.validate package. A {@link ValidationResult} is returned which contains a list of
27 |    * violations. If the list is empty, the message is valid. If the list is non-empty, the message
28 |    * is invalid. An exception is thrown if the message cannot be validated because the evaluation
29 |    * logic for the message cannot be built ({@link CompilationException}), or there is a type error
30 |    * when attempting to evaluate a CEL expression associated with the message ({@link
31 |    * ExecutionException}).
32 |    *
33 |    * @param msg the {@link Message} to be validated.
34 |    * @return the {@link ValidationResult} from the evaluation.
35 |    * @throws ValidationException if there are any compilation or validation execution errors.
36 |    */
37 |   ValidationResult validate(Message msg) throws ValidationException;
38 | }
39 | 


--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ValidatorFactory.java:
--------------------------------------------------------------------------------
  1 | // Copyright 2023-2025 Buf Technologies, Inc.
  2 | //
  3 | // Licensed under the Apache License, Version 2.0 (the "License");
  4 | // you may not use this file except in compliance with the License.
  5 | // You may obtain a copy of the License at
  6 | //
  7 | //      http://www.apache.org/licenses/LICENSE-2.0
  8 | //
  9 | // Unless required by applicable law or agreed to in writing, software
 10 | // distributed under the License is distributed on an "AS IS" BASIS,
 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 | // See the License for the specific language governing permissions and
 13 | // limitations under the License.
 14 | 
 15 | package build.buf.protovalidate;
 16 | 
 17 | import build.buf.protovalidate.exceptions.CompilationException;
 18 | import com.google.protobuf.Descriptors.Descriptor;
 19 | import java.util.List;
 20 | import org.jspecify.annotations.Nullable;
 21 | 
 22 | /**
 23 |  * ValidatorFactory is used to create a validator.
 24 |  *
 25 |  * 

Validators can be created with an optional {@link Config} to customize behavior. They can also 26 | * be created with a list of seed descriptors to warmup the validator cache ahead of time as well as 27 | * an indicator to lazily-load any descriptors not provided into the cache. 28 | */ 29 | public final class ValidatorFactory { 30 | // Prevent instantiation 31 | private ValidatorFactory() {} 32 | 33 | /** A builder class used for building a validator. */ 34 | public static class ValidatorBuilder { 35 | /** The config object to use for instantiating a validator. */ 36 | @Nullable private Config config; 37 | 38 | /** 39 | * Create a validator with the given config 40 | * 41 | * @param config The {@link Config} to configure the validator. 42 | * @return The builder instance 43 | */ 44 | public ValidatorBuilder withConfig(Config config) { 45 | this.config = config; 46 | return this; 47 | } 48 | 49 | // Prevent instantiation 50 | private ValidatorBuilder() {} 51 | 52 | /** 53 | * Build a new validator 54 | * 55 | * @return A new {@link Validator} instance. 56 | */ 57 | public Validator build() { 58 | Config cfg = this.config; 59 | if (cfg == null) { 60 | cfg = Config.newBuilder().build(); 61 | } 62 | return new ValidatorImpl(cfg); 63 | } 64 | 65 | /** 66 | * Build the validator, warming up the cache with any provided descriptors. 67 | * 68 | * @param descriptors the list of descriptors to warm up the cache. 69 | * @param disableLazy whether to disable lazy loading of validation rules. When validation is 70 | * performed, a message's rules will be looked up in a cache. If they are not found, by 71 | * default they will be processed and lazily-loaded into the cache. Setting this to false 72 | * will not attempt to lazily-load descriptor information not found in the cache and 73 | * essentially makes the entire cache read-only, eliminating thread contention. 74 | * @return A new {@link Validator} instance. 75 | * @throws CompilationException If any of the given descriptors' validation rules fail 76 | * processing while warming up the cache. 77 | * @throws IllegalStateException If disableLazy is set to true and no descriptors are passed. 78 | */ 79 | public Validator buildWithDescriptors(List descriptors, boolean disableLazy) 80 | throws CompilationException, IllegalStateException { 81 | if (disableLazy && (descriptors == null || descriptors.isEmpty())) { 82 | throw new IllegalStateException( 83 | "a list of descriptors is required when disableLazy is true"); 84 | } 85 | 86 | Config cfg = this.config; 87 | if (cfg == null) { 88 | cfg = Config.newBuilder().build(); 89 | } 90 | return new ValidatorImpl(cfg, descriptors, disableLazy); 91 | } 92 | } 93 | 94 | /** 95 | * Creates a new builder for a validator. 96 | * 97 | * @return A Validator builder 98 | */ 99 | public static ValidatorBuilder newBuilder() { 100 | return new ValidatorBuilder(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/ValidatorImpl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.CompilationException; 18 | import build.buf.protovalidate.exceptions.ValidationException; 19 | import com.google.protobuf.Descriptors.Descriptor; 20 | import com.google.protobuf.Message; 21 | import dev.cel.bundle.Cel; 22 | import dev.cel.bundle.CelFactory; 23 | import dev.cel.common.CelOptions; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | final class ValidatorImpl implements Validator { 28 | /** evaluatorBuilder is the builder used to construct the evaluator for a given message. */ 29 | private final EvaluatorBuilder evaluatorBuilder; 30 | 31 | /** 32 | * failFast indicates whether the validator should stop evaluating rules after the first 33 | * violation. 34 | */ 35 | private final boolean failFast; 36 | 37 | ValidatorImpl(Config config) { 38 | ValidateLibrary validateLibrary = new ValidateLibrary(); 39 | Cel cel = 40 | CelFactory.standardCelBuilder() 41 | .addCompilerLibraries(validateLibrary) 42 | .addRuntimeLibraries(validateLibrary) 43 | .setOptions( 44 | CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build()) 45 | .build(); 46 | this.evaluatorBuilder = new EvaluatorBuilder(cel, config); 47 | this.failFast = config.isFailFast(); 48 | } 49 | 50 | ValidatorImpl(Config config, List descriptors, boolean disableLazy) 51 | throws CompilationException { 52 | ValidateLibrary validateLibrary = new ValidateLibrary(); 53 | Cel cel = 54 | CelFactory.standardCelBuilder() 55 | .addCompilerLibraries(validateLibrary) 56 | .addRuntimeLibraries(validateLibrary) 57 | .setOptions( 58 | CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build()) 59 | .build(); 60 | this.evaluatorBuilder = new EvaluatorBuilder(cel, config, descriptors, disableLazy); 61 | this.failFast = config.isFailFast(); 62 | } 63 | 64 | @Override 65 | public ValidationResult validate(Message msg) throws ValidationException { 66 | if (msg == null) { 67 | return ValidationResult.EMPTY; 68 | } 69 | Descriptor descriptor = msg.getDescriptorForType(); 70 | Evaluator evaluator = evaluatorBuilder.load(descriptor); 71 | List result = evaluator.evaluate(new MessageValue(msg), this.failFast); 72 | if (result.isEmpty()) { 73 | return ValidationResult.EMPTY; 74 | } 75 | List violations = new ArrayList<>(result.size()); 76 | for (RuleViolation.Builder builder : result) { 77 | violations.add(builder.build()); 78 | } 79 | return new ValidationResult(violations); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Value.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import com.google.protobuf.Descriptors; 18 | import com.google.protobuf.Message; 19 | import java.util.List; 20 | import java.util.Map; 21 | import org.jspecify.annotations.Nullable; 22 | 23 | /** 24 | * {@link Value} is a wrapper around a protobuf value that provides helper methods for accessing the 25 | * value. 26 | */ 27 | interface Value { 28 | /** 29 | * Get the field descriptor that corresponds to the underlying Value, if it is a message field. 30 | * 31 | * @return The underlying {@link Descriptors.FieldDescriptor}. null if the underlying value is not 32 | * a message field. 33 | */ 34 | Descriptors.@Nullable FieldDescriptor fieldDescriptor(); 35 | 36 | /** 37 | * Get the underlying value as a {@link Message} type. 38 | * 39 | * @return The underlying {@link Message} value. null if the underlying value is not a {@link 40 | * Message} type. 41 | */ 42 | @Nullable Message messageValue(); 43 | 44 | /** 45 | * Get the underlying value and cast it to the class type. 46 | * 47 | * @param clazz The inferred class. 48 | * @return The value casted to the inferred class type. 49 | * @param The class type. 50 | */ 51 | T value(Class clazz); 52 | 53 | /** 54 | * Get the underlying value as a list. 55 | * 56 | * @return The underlying value as a list. Empty list is returned if the underlying type is not a 57 | * list. 58 | */ 59 | List repeatedValue(); 60 | 61 | /** 62 | * Get the underlying value as a map. 63 | * 64 | * @return The underlying value as a map. Empty map is returned if the underlying type is not a 65 | * list. 66 | */ 67 | Map mapValue(); 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/ValueEvaluator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import build.buf.protovalidate.exceptions.ExecutionException; 18 | import build.buf.validate.FieldPath; 19 | import com.google.protobuf.Descriptors; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import org.jspecify.annotations.Nullable; 24 | 25 | /** 26 | * {@link ValueEvaluator} performs validation on any concrete value contained within a singular 27 | * field, repeated elements, or the keys/values of a map. 28 | */ 29 | final class ValueEvaluator implements Evaluator { 30 | /** The {@link Descriptors.FieldDescriptor} targeted by this evaluator */ 31 | private final Descriptors.@Nullable FieldDescriptor descriptor; 32 | 33 | /** The nested rule path that this value evaluator is for */ 34 | @Nullable private final FieldPath nestedRule; 35 | 36 | /** The default or zero-value for this value's type. */ 37 | @Nullable private Object zero; 38 | 39 | /** The evaluators applied to a value. */ 40 | private final List evaluators = new ArrayList<>(); 41 | 42 | /** 43 | * Indicates that the Rules should not be applied if the field is unset or the default (typically 44 | * zero) value. 45 | */ 46 | private boolean ignoreEmpty; 47 | 48 | /** Constructs a {@link ValueEvaluator}. */ 49 | ValueEvaluator(Descriptors.@Nullable FieldDescriptor descriptor, @Nullable FieldPath nestedRule) { 50 | this.descriptor = descriptor; 51 | this.nestedRule = nestedRule; 52 | } 53 | 54 | Descriptors.@Nullable FieldDescriptor getDescriptor() { 55 | return descriptor; 56 | } 57 | 58 | @Nullable FieldPath getNestedRule() { 59 | return nestedRule; 60 | } 61 | 62 | boolean hasNestedRule() { 63 | return this.nestedRule != null; 64 | } 65 | 66 | @Override 67 | public boolean tautology() { 68 | return evaluators.isEmpty(); 69 | } 70 | 71 | @Override 72 | public List evaluate(Value val, boolean failFast) 73 | throws ExecutionException { 74 | if (this.shouldIgnore(val.value(Object.class))) { 75 | return RuleViolation.NO_VIOLATIONS; 76 | } 77 | List allViolations = new ArrayList<>(); 78 | for (Evaluator evaluator : evaluators) { 79 | List violations = evaluator.evaluate(val, failFast); 80 | if (failFast && !violations.isEmpty()) { 81 | return violations; 82 | } 83 | allViolations.addAll(violations); 84 | } 85 | if (allViolations.isEmpty()) { 86 | return RuleViolation.NO_VIOLATIONS; 87 | } 88 | return allViolations; 89 | } 90 | 91 | /** 92 | * Appends an evaluator to the list of evaluators. 93 | * 94 | * @param eval The evaluator to append. 95 | */ 96 | void append(Evaluator eval) { 97 | if (!eval.tautology()) { 98 | this.evaluators.add(eval); 99 | } 100 | } 101 | 102 | void setIgnoreEmpty(Object zero) { 103 | this.ignoreEmpty = true; 104 | this.zero = zero; 105 | } 106 | 107 | private boolean shouldIgnore(Object value) { 108 | return this.ignoreEmpty && Objects.equals(value, this.zero); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Variable.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import dev.cel.runtime.CelVariableResolver; 18 | import java.util.Optional; 19 | import org.jspecify.annotations.Nullable; 20 | 21 | /** 22 | * {@link Variable} implements {@link CelVariableResolver}, providing a lightweight named variable 23 | * to cel.Program executions. 24 | */ 25 | final class Variable implements CelVariableResolver { 26 | /** The {@value} variable in CEL. */ 27 | static final String THIS_NAME = "this"; 28 | 29 | /** The {@value} variable in CEL. */ 30 | static final String RULES_NAME = "rules"; 31 | 32 | /** The {@value} variable in CEL. */ 33 | static final String RULE_NAME = "rule"; 34 | 35 | /** The variable's name */ 36 | private final String name; 37 | 38 | /** The value for this variable */ 39 | @Nullable private final Object val; 40 | 41 | /** Creates a variable with the given name and value. */ 42 | private Variable(String name, @Nullable Object val) { 43 | this.name = name; 44 | this.val = val; 45 | } 46 | 47 | /** 48 | * Creates a "this" variable. 49 | * 50 | * @param val the value. 51 | * @return {@link Variable}. 52 | */ 53 | static CelVariableResolver newThisVariable(@Nullable Object val) { 54 | return CelVariableResolver.hierarchicalVariableResolver( 55 | new NowVariable(), new Variable(THIS_NAME, val)); 56 | } 57 | 58 | /** 59 | * Creates a "rules" variable. 60 | * 61 | * @param val the value. 62 | * @return {@link Variable}. 63 | */ 64 | static CelVariableResolver newRulesVariable(Object val) { 65 | return new Variable(RULES_NAME, val); 66 | } 67 | 68 | /** 69 | * Creates a "rule" variable. 70 | * 71 | * @param rules the value of the "rules" variable. 72 | * @param val the value of the "rule" variable. 73 | * @return {@link Variable}. 74 | */ 75 | static CelVariableResolver newRuleVariable(Object rules, Object val) { 76 | return CelVariableResolver.hierarchicalVariableResolver( 77 | newRulesVariable(rules), new Variable(RULE_NAME, val)); 78 | } 79 | 80 | @Override 81 | public Optional find(String name) { 82 | if (!this.name.equals(name) || val == null) { 83 | return Optional.empty(); 84 | } 85 | return Optional.of(val); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/Violation.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import com.google.protobuf.Descriptors; 18 | import org.jspecify.annotations.Nullable; 19 | 20 | /** {@link Violation} provides all the collected information about an individual rule violation. */ 21 | public interface Violation { 22 | /** {@link FieldValue} represents a Protobuf field value inside a Protobuf message. */ 23 | interface FieldValue { 24 | /** 25 | * Gets the value of the field, which may be null, a primitive, a Map or a List. 26 | * 27 | * @return The value of the protobuf field. 28 | */ 29 | @Nullable Object getValue(); 30 | 31 | /** 32 | * Gets the field descriptor of the field this value is from. 33 | * 34 | * @return A FieldDescriptor pertaining to this field. 35 | */ 36 | Descriptors.FieldDescriptor getDescriptor(); 37 | } 38 | 39 | /** 40 | * Gets the protobuf form of this violation. 41 | * 42 | * @return The protobuf form of this violation. 43 | */ 44 | build.buf.validate.Violation toProto(); 45 | 46 | /** 47 | * Gets the value of the field this violation pertains to, or null if there is none. 48 | * 49 | * @return Value of the field associated with the violation, or null if there is none. 50 | */ 51 | @Nullable FieldValue getFieldValue(); 52 | 53 | /** 54 | * Gets the value of the rule this violation pertains to, or null if there is none. 55 | * 56 | * @return Value of the rule associated with the violation, or null if there is none. 57 | */ 58 | @Nullable FieldValue getRuleValue(); 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/exceptions/CompilationException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate.exceptions; 16 | 17 | /** CompilationException is returned when a rule fails to compile. This is a fatal error. */ 18 | public class CompilationException extends ValidationException { 19 | /** 20 | * Creates a CompilationException with the specified message. 21 | * 22 | * @param message Exception message. 23 | */ 24 | public CompilationException(String message) { 25 | super(message); 26 | } 27 | 28 | /** 29 | * Creates a CompilationException with the specified message and cause. 30 | * 31 | * @param message Exception message. 32 | * @param cause Underlying cause of the exception. 33 | */ 34 | public CompilationException(String message, Throwable cause) { 35 | super(message, cause); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/exceptions/ExecutionException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate.exceptions; 16 | 17 | /** ExecutionException is returned when a rule fails to execute. This is a fatal error. */ 18 | public class ExecutionException extends ValidationException { 19 | /** 20 | * Creates an ExecutionException with the specified message. 21 | * 22 | * @param message Exception message. 23 | */ 24 | public ExecutionException(String message) { 25 | super(message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/build/buf/protovalidate/exceptions/ValidationException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate.exceptions; 16 | 17 | /** ValidationException is the base exception for all validation errors. */ 18 | public class ValidationException extends Exception { 19 | /** 20 | * Creates a ValidationException with the specified message. 21 | * 22 | * @param message Exception message. 23 | */ 24 | public ValidationException(String message) { 25 | super(message); 26 | } 27 | 28 | /** 29 | * Creates a ValidationException with the specified message and cause. 30 | * 31 | * @param message Exception message. 32 | * @param cause Underlying cause of the exception. 33 | */ 34 | public ValidationException(String message, Throwable cause) { 35 | super(message, cause); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/build/buf/protovalidate/FormatTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import static org.assertj.core.api.Assertions.*; 18 | 19 | import cel.expr.conformance.proto3.TestAllTypes; 20 | import com.cel.expr.Decl; 21 | import com.cel.expr.ExprValue; 22 | import com.cel.expr.Value; 23 | import com.cel.expr.conformance.test.SimpleTest; 24 | import com.cel.expr.conformance.test.SimpleTestFile; 25 | import com.cel.expr.conformance.test.SimpleTestSection; 26 | import com.google.protobuf.TextFormat; 27 | import dev.cel.bundle.Cel; 28 | import dev.cel.bundle.CelBuilder; 29 | import dev.cel.bundle.CelFactory; 30 | import dev.cel.common.CelOptions; 31 | import dev.cel.common.CelValidationException; 32 | import dev.cel.common.CelValidationResult; 33 | import dev.cel.common.types.SimpleType; 34 | import dev.cel.runtime.CelEvaluationException; 35 | import dev.cel.runtime.CelRuntime.Program; 36 | import java.nio.charset.StandardCharsets; 37 | import java.nio.file.Files; 38 | import java.nio.file.Paths; 39 | import java.util.ArrayList; 40 | import java.util.HashMap; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.stream.Collectors; 44 | import java.util.stream.Stream; 45 | import org.junit.jupiter.api.BeforeAll; 46 | import org.junit.jupiter.api.Named; 47 | import org.junit.jupiter.params.ParameterizedTest; 48 | import org.junit.jupiter.params.provider.Arguments; 49 | import org.junit.jupiter.params.provider.MethodSource; 50 | 51 | class FormatTest { 52 | // Version of the cel-spec that this implementation is conformant with 53 | // This should be kept in sync with the version in gradle.properties 54 | private static final String CEL_SPEC_VERSION = "v0.24.0"; 55 | 56 | private static Cel cel; 57 | 58 | private static List formatTests; 59 | private static List formatErrorTests; 60 | 61 | @BeforeAll 62 | public static void setUp() throws Exception { 63 | // The test data from the cel-spec conformance tests 64 | List celSpecSections = 65 | loadTestData("src/test/resources/testdata/string_ext_" + CEL_SPEC_VERSION + ".textproto"); 66 | // Our supplemental tests of functionality not in the cel conformance file, but defined in the 67 | // spec. 68 | List supplementalSections = 69 | loadTestData("src/test/resources/testdata/string_ext_supplemental.textproto"); 70 | 71 | // Combine the test data from both files into one 72 | List sections = 73 | Stream.concat(celSpecSections.stream(), supplementalSections.stream()) 74 | .collect(Collectors.toList()); 75 | 76 | // Find the format tests which test successful formatting 77 | formatTests = 78 | sections.stream() 79 | .filter(s -> s.getName().equals("format")) 80 | .flatMap(s -> s.getTestList().stream()) 81 | .collect(Collectors.toList()); 82 | 83 | // Find the format error tests which test errors during formatting 84 | formatErrorTests = 85 | sections.stream() 86 | .filter(s -> s.getName().equals("format_errors")) 87 | .flatMap(s -> s.getTestList().stream()) 88 | .collect(Collectors.toList()); 89 | 90 | ValidateLibrary validateLibrary = new ValidateLibrary(); 91 | cel = 92 | CelFactory.standardCelBuilder() 93 | .addCompilerLibraries(validateLibrary) 94 | .addRuntimeLibraries(validateLibrary) 95 | .setOptions( 96 | CelOptions.DEFAULT.toBuilder().evaluateCanonicalTypesToNativeValues(true).build()) 97 | .build(); 98 | } 99 | 100 | @ParameterizedTest 101 | @MethodSource("getFormatTests") 102 | void testFormatSuccess(SimpleTest test) throws CelValidationException, CelEvaluationException { 103 | Object result = evaluate(test); 104 | assertThat(result).isEqualTo(getExpectedResult(test)); 105 | assertThat(result).isInstanceOf(String.class); 106 | } 107 | 108 | @ParameterizedTest 109 | @MethodSource("getFormatErrorTests") 110 | void testFormatError(SimpleTest test) { 111 | assertThatThrownBy(() -> evaluate(test)).isInstanceOf(CelEvaluationException.class); 112 | } 113 | 114 | // Loads test data from the given text format file 115 | private static List loadTestData(String fileName) throws Exception { 116 | byte[] encoded = Files.readAllBytes(Paths.get(fileName)); 117 | String data = new String(encoded, StandardCharsets.UTF_8); 118 | SimpleTestFile.Builder bldr = SimpleTestFile.newBuilder(); 119 | TextFormat.getParser().merge(data, bldr); 120 | SimpleTestFile testData = bldr.build(); 121 | 122 | return testData.getSectionList(); 123 | } 124 | 125 | // Runs a test by extending the cel environment with the specified 126 | // types, variables and declarations, then evaluating it with the cel runtime. 127 | private static Object evaluate(SimpleTest test) 128 | throws CelValidationException, CelEvaluationException { 129 | 130 | CelBuilder builder = cel.toCelBuilder().addMessageTypes(TestAllTypes.getDescriptor()); 131 | addDecls(builder, test); 132 | Cel newCel = builder.build(); 133 | 134 | CelValidationResult validationResult = newCel.compile(test.getExpr()); 135 | if (!validationResult.getAllIssues().isEmpty()) { 136 | fail("error building AST for evaluation: " + validationResult.getIssueString()); 137 | } 138 | Program program = newCel.createProgram(validationResult.getAst()); 139 | return program.eval(buildVariables(test.getBindingsMap())); 140 | } 141 | 142 | private static Stream getTestStream(List tests) { 143 | List args = new ArrayList<>(); 144 | for (SimpleTest test : tests) { 145 | args.add(Arguments.arguments(Named.named(test.getName(), test))); 146 | } 147 | 148 | return args.stream(); 149 | } 150 | 151 | private static Stream getFormatTests() { 152 | return getTestStream(formatTests); 153 | } 154 | 155 | private static Stream getFormatErrorTests() { 156 | return getTestStream(formatErrorTests); 157 | } 158 | 159 | // Builds the variable definitions to be used during evaluation 160 | private static Map buildVariables(Map bindings) { 161 | Map vars = new HashMap<>(); 162 | for (Map.Entry entry : bindings.entrySet()) { 163 | ExprValue exprValue = entry.getValue(); 164 | if (exprValue.hasValue()) { 165 | Value val = exprValue.getValue(); 166 | if (val.hasStringValue()) { 167 | vars.put(entry.getKey(), val.getStringValue()); 168 | } 169 | } 170 | } 171 | return vars; 172 | } 173 | 174 | // Gets the expected result for a given test 175 | private static String getExpectedResult(SimpleTest test) { 176 | if (test.hasValue()) { 177 | if (test.getValue().hasStringValue()) { 178 | return test.getValue().getStringValue(); 179 | } 180 | } else if (test.hasEvalError()) { 181 | // Note that we only expect a single eval error for all the conformance tests 182 | if (test.getEvalError().getErrorsList().size() == 1) { 183 | return test.getEvalError().getErrorsList().get(0).getMessage(); 184 | } 185 | } 186 | return ""; 187 | } 188 | 189 | // Builds the declarations for a given test 190 | private static void addDecls(CelBuilder builder, SimpleTest test) { 191 | for (Decl decl : test.getTypeEnvList()) { 192 | if (decl.hasIdent()) { 193 | Decl.IdentDecl ident = decl.getIdent(); 194 | com.cel.expr.Type type = ident.getType(); 195 | if (type.hasPrimitive()) { 196 | if (type.getPrimitive() == com.cel.expr.Type.PrimitiveType.STRING) { 197 | builder.addVar(decl.getName(), SimpleType.STRING); 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/test/java/build/buf/protovalidate/ValidationResultTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import build.buf.validate.FieldPathElement; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import org.junit.jupiter.api.Test; 23 | 24 | class ValidationResultTest { 25 | @Test 26 | void testToStringNoViolations() { 27 | 28 | List violations = new ArrayList<>(); 29 | ValidationResult result = new ValidationResult(violations); 30 | 31 | assertThat(result.toString()).isEqualTo("Validation OK"); 32 | assertThat(result.isSuccess()).isTrue(); 33 | } 34 | 35 | @Test 36 | void testToStringSingleViolation() { 37 | FieldPathElement elem = 38 | FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("test_field_name").build(); 39 | 40 | RuleViolation violation = 41 | RuleViolation.newBuilder() 42 | .setRuleId("int32.const") 43 | .setMessage("value must equal 42") 44 | .addFirstFieldPathElement(elem) 45 | .build(); 46 | List violations = new ArrayList<>(); 47 | violations.add(violation); 48 | ValidationResult result = new ValidationResult(violations); 49 | 50 | assertThat(result.toString()) 51 | .isEqualTo("Validation error:\n - test_field_name: value must equal 42 [int32.const]"); 52 | } 53 | 54 | @Test 55 | void testToStringMultipleViolations() { 56 | FieldPathElement elem = 57 | FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("test_field_name").build(); 58 | 59 | RuleViolation violation1 = 60 | RuleViolation.newBuilder() 61 | .setRuleId("int32.const") 62 | .setMessage("value must equal 42") 63 | .addFirstFieldPathElement(elem) 64 | .build(); 65 | 66 | RuleViolation violation2 = 67 | RuleViolation.newBuilder() 68 | .setRuleId("int32.required") 69 | .setMessage("value is required") 70 | .addFirstFieldPathElement(elem) 71 | .build(); 72 | List violations = new ArrayList<>(); 73 | violations.add(violation1); 74 | violations.add(violation2); 75 | ValidationResult result = new ValidationResult(violations); 76 | 77 | assertThat(result.toString()) 78 | .isEqualTo( 79 | "Validation error:\n - test_field_name: value must equal 42 [int32.const]\n - test_field_name: value is required [int32.required]"); 80 | } 81 | 82 | @Test 83 | void testToStringSingleViolationMultipleFieldPathElements() { 84 | FieldPathElement elem1 = 85 | FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("test_field_name").build(); 86 | FieldPathElement elem2 = 87 | FieldPathElement.newBuilder().setFieldNumber(5).setFieldName("nested_name").build(); 88 | 89 | List elems = new ArrayList<>(); 90 | elems.add(elem1); 91 | elems.add(elem2); 92 | 93 | RuleViolation violation1 = 94 | RuleViolation.newBuilder() 95 | .setRuleId("int32.const") 96 | .setMessage("value must equal 42") 97 | .addAllFieldPathElements(elems) 98 | .build(); 99 | 100 | List violations = new ArrayList<>(); 101 | violations.add(violation1); 102 | ValidationResult result = new ValidationResult(violations); 103 | 104 | assertThat(result.toString()) 105 | .isEqualTo( 106 | "Validation error:\n - test_field_name.nested_name: value must equal 42 [int32.const]"); 107 | } 108 | 109 | @Test 110 | void testToStringSingleViolationNoFieldPathElements() { 111 | RuleViolation violation = 112 | RuleViolation.newBuilder() 113 | .setRuleId("int32.const") 114 | .setMessage("value must equal 42") 115 | .build(); 116 | List violations = new ArrayList<>(); 117 | violations.add(violation); 118 | ValidationResult result = new ValidationResult(violations); 119 | 120 | assertThat(result.toString()) 121 | .isEqualTo("Validation error:\n - value must equal 42 [int32.const]"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/build/buf/protovalidate/ValidatorCelExpressionTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import build.buf.validate.FieldPath; 20 | import build.buf.validate.FieldRules; 21 | import build.buf.validate.Violation; 22 | import com.example.imports.buf.validate.RepeatedRules; 23 | import java.util.Arrays; 24 | import org.junit.jupiter.api.Test; 25 | 26 | /** This test verifies that custom (CEL-based) field and/or message rules evaluate as expected. */ 27 | public class ValidatorCelExpressionTest { 28 | 29 | @Test 30 | public void testFieldExpressionRepeatedMessage() throws Exception { 31 | // Nested message wrapping the int 1 32 | com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg one = 33 | com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg.newBuilder() 34 | .setA(1) 35 | .build(); 36 | 37 | // Nested message wrapping the int 2 38 | com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg two = 39 | com.example.imports.validationtest.FieldExpressionRepeatedMessage.Msg.newBuilder() 40 | .setA(2) 41 | .build(); 42 | 43 | // Create a valid message (1, 1) 44 | com.example.imports.validationtest.FieldExpressionRepeatedMessage validMsg = 45 | com.example.imports.validationtest.FieldExpressionRepeatedMessage.newBuilder() 46 | .addAllVal(Arrays.asList(one, one)) 47 | .build(); 48 | 49 | // Create an invalid message (1, 2, 1) 50 | com.example.imports.validationtest.FieldExpressionRepeatedMessage invalidMsg = 51 | com.example.imports.validationtest.FieldExpressionRepeatedMessage.newBuilder() 52 | .addAllVal(Arrays.asList(one, two, one)) 53 | .build(); 54 | 55 | // Build a model of the expected violation 56 | Violation expectedViolation = 57 | Violation.newBuilder() 58 | .setField( 59 | FieldPath.newBuilder() 60 | .addElements( 61 | FieldPathUtils.fieldPathElement( 62 | invalidMsg.getDescriptorForType().findFieldByName("val")) 63 | .toBuilder() 64 | .build())) 65 | .setRule( 66 | FieldPath.newBuilder() 67 | .addElements( 68 | FieldPathUtils.fieldPathElement( 69 | FieldRules.getDescriptor() 70 | .findFieldByNumber(FieldRules.CEL_FIELD_NUMBER)) 71 | .toBuilder() 72 | .setIndex(0) 73 | .build())) 74 | .setRuleId("field_expression.repeated.message") 75 | .setMessage("test message field_expression.repeated.message") 76 | .build(); 77 | 78 | Validator validator = ValidatorFactory.newBuilder().build(); 79 | 80 | // Valid message checks 81 | ValidationResult validResult = validator.validate(validMsg); 82 | assertThat(validResult.isSuccess()).isTrue(); 83 | 84 | // Invalid message checks 85 | ValidationResult invalidResult = validator.validate(invalidMsg); 86 | assertThat(invalidResult.isSuccess()).isFalse(); 87 | assertThat(invalidResult.toProto().getViolationsList()).containsExactly(expectedViolation); 88 | } 89 | 90 | @Test 91 | public void testFieldExpressionRepeatedMessageItems() throws Exception { 92 | // Nested message wrapping the int 1 93 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg one = 94 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg.newBuilder() 95 | .setA(1) 96 | .build(); 97 | 98 | // Nested message wrapping the int 2 99 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg two = 100 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.Msg.newBuilder() 101 | .setA(2) 102 | .build(); 103 | 104 | // Create a valid message (1, 1) 105 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems validMsg = 106 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.newBuilder() 107 | .addAllVal(Arrays.asList(one, one)) 108 | .build(); 109 | 110 | // Create an invalid message (1, 2, 1) 111 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems invalidMsg = 112 | com.example.imports.validationtest.FieldExpressionRepeatedMessageItems.newBuilder() 113 | .addAllVal(Arrays.asList(one, two, one)) 114 | .build(); 115 | 116 | // Build a model of the expected violation 117 | Violation expectedViolation = 118 | Violation.newBuilder() 119 | .setField( 120 | FieldPath.newBuilder() 121 | .addElements( 122 | FieldPathUtils.fieldPathElement( 123 | invalidMsg.getDescriptorForType().findFieldByName("val")) 124 | .toBuilder() 125 | .setIndex(1) 126 | .build())) 127 | .setRule( 128 | FieldPath.newBuilder() 129 | .addElements( 130 | FieldPathUtils.fieldPathElement( 131 | FieldRules.getDescriptor() 132 | .findFieldByNumber(FieldRules.REPEATED_FIELD_NUMBER))) 133 | .addElements( 134 | FieldPathUtils.fieldPathElement( 135 | RepeatedRules.getDescriptor().findFieldByName("items"))) 136 | .addElements( 137 | FieldPathUtils.fieldPathElement( 138 | FieldRules.getDescriptor() 139 | .findFieldByNumber(FieldRules.CEL_FIELD_NUMBER)) 140 | .toBuilder() 141 | .setIndex(0) 142 | .build())) 143 | .setRuleId("field_expression.repeated.message.items") 144 | .setMessage("test message field_expression.repeated.message.items") 145 | .build(); 146 | 147 | Validator validator = ValidatorFactory.newBuilder().build(); 148 | 149 | // Valid message checks 150 | ValidationResult validResult = validator.validate(validMsg); 151 | assertThat(validResult.isSuccess()).isTrue(); 152 | 153 | // Invalid message checks 154 | ValidationResult invalidResult = validator.validate(invalidMsg); 155 | assertThat(invalidResult.isSuccess()).isFalse(); 156 | assertThat(invalidResult.toProto().getViolationsList()).containsExactly(expectedViolation); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/build/buf/protovalidate/ValidatorImportTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package build.buf.protovalidate; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | import com.example.imports.validationtest.ExampleImportMessage; 20 | import com.example.imports.validationtest.ExampleImportMessageFieldRule; 21 | import com.example.imports.validationtest.ExampleImportMessageInMap; 22 | import com.example.imports.validationtest.ExampleImportMessageInMapFieldRule; 23 | import com.example.imports.validationtest.ExampleImportedMessage; 24 | import org.junit.jupiter.api.Test; 25 | 26 | public class ValidatorImportTest { 27 | @Test 28 | public void testImportedMessageFromAnotherFile() throws Exception { 29 | com.example.imports.validationtest.ExampleImportMessage valid = 30 | ExampleImportMessage.newBuilder() 31 | .setImportedSubmessage( 32 | ExampleImportedMessage.newBuilder().setHexString("0123456789abcdef").build()) 33 | .build(); 34 | assertThat( 35 | ValidatorFactory.newBuilder() 36 | .build() 37 | .validate(valid) 38 | .toProto() 39 | .getViolationsList() 40 | .size()) 41 | .isEqualTo(0); 42 | 43 | com.example.imports.validationtest.ExampleImportMessage invalid = 44 | ExampleImportMessage.newBuilder() 45 | .setImportedSubmessage(ExampleImportedMessage.newBuilder().setHexString("zyx").build()) 46 | .build(); 47 | assertThat( 48 | ValidatorFactory.newBuilder() 49 | .build() 50 | .validate(invalid) 51 | .toProto() 52 | .getViolationsList() 53 | .size()) 54 | .isEqualTo(1); 55 | } 56 | 57 | @Test 58 | public void testImportedMessageFromAnotherFileInField() throws Exception { 59 | com.example.imports.validationtest.ExampleImportMessageFieldRule valid = 60 | ExampleImportMessageFieldRule.newBuilder() 61 | .setMessageWithImport( 62 | ExampleImportMessage.newBuilder() 63 | .setImportedSubmessage( 64 | ExampleImportedMessage.newBuilder() 65 | .setHexString("0123456789abcdef") 66 | .build()) 67 | .build()) 68 | .build(); 69 | assertThat( 70 | ValidatorFactory.newBuilder() 71 | .build() 72 | .validate(valid) 73 | .toProto() 74 | .getViolationsList() 75 | .size()) 76 | .isEqualTo(0); 77 | 78 | com.example.imports.validationtest.ExampleImportMessageFieldRule invalid = 79 | ExampleImportMessageFieldRule.newBuilder() 80 | .setMessageWithImport( 81 | ExampleImportMessage.newBuilder() 82 | .setImportedSubmessage( 83 | ExampleImportedMessage.newBuilder().setHexString("zyx").build()) 84 | .build()) 85 | .build(); 86 | assertThat( 87 | ValidatorFactory.newBuilder() 88 | .build() 89 | .validate(invalid) 90 | .toProto() 91 | .getViolationsList() 92 | .size()) 93 | .isEqualTo(1); 94 | } 95 | 96 | @Test 97 | public void testImportedMessageFromAnotherFileInMap() throws Exception { 98 | com.example.imports.validationtest.ExampleImportMessageInMap valid = 99 | ExampleImportMessageInMap.newBuilder() 100 | .putImportedSubmessage( 101 | 0, ExampleImportedMessage.newBuilder().setHexString("0123456789abcdef").build()) 102 | .build(); 103 | assertThat( 104 | ValidatorFactory.newBuilder() 105 | .build() 106 | .validate(valid) 107 | .toProto() 108 | .getViolationsList() 109 | .size()) 110 | .isEqualTo(0); 111 | 112 | com.example.imports.validationtest.ExampleImportMessageInMap invalid = 113 | ExampleImportMessageInMap.newBuilder() 114 | .putImportedSubmessage( 115 | 0, ExampleImportedMessage.newBuilder().setHexString("zyx").build()) 116 | .build(); 117 | assertThat( 118 | ValidatorFactory.newBuilder() 119 | .build() 120 | .validate(invalid) 121 | .toProto() 122 | .getViolationsList() 123 | .size()) 124 | .isEqualTo(1); 125 | } 126 | 127 | @Test 128 | public void testImportedMessageFromAnotherFileInMapInField() throws Exception { 129 | com.example.imports.validationtest.ExampleImportMessageInMapFieldRule valid = 130 | ExampleImportMessageInMapFieldRule.newBuilder() 131 | .setMessageWithImport( 132 | ExampleImportMessageInMap.newBuilder() 133 | .putImportedSubmessage( 134 | 0, 135 | ExampleImportedMessage.newBuilder() 136 | .setHexString("0123456789abcdef") 137 | .build()) 138 | .build()) 139 | .build(); 140 | assertThat( 141 | ValidatorFactory.newBuilder() 142 | .build() 143 | .validate(valid) 144 | .toProto() 145 | .getViolationsList() 146 | .size()) 147 | .isEqualTo(0); 148 | 149 | com.example.imports.validationtest.ExampleImportMessageInMapFieldRule invalid = 150 | ExampleImportMessageInMapFieldRule.newBuilder() 151 | .setMessageWithImport( 152 | ExampleImportMessageInMap.newBuilder() 153 | .putImportedSubmessage( 154 | 0, ExampleImportedMessage.newBuilder().setHexString("zyx").build()) 155 | .build()) 156 | .build(); 157 | assertThat( 158 | ValidatorFactory.newBuilder() 159 | .build() 160 | .validate(invalid) 161 | .toProto() 162 | .getViolationsList() 163 | .size()) 164 | .isEqualTo(1); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/test/resources/proto/buf.gen.cel.testtypes.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | override: 5 | - file_option: java_package 6 | value: cel.expr.conformance.proto3 7 | plugins: 8 | - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion 9 | out: build/generated/test-sources/bufgen 10 | -------------------------------------------------------------------------------- /src/test/resources/proto/buf.gen.cel.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | plugins: 5 | - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion 6 | out: build/generated/test-sources/bufgen 7 | -------------------------------------------------------------------------------- /src/test/resources/proto/buf.gen.imports.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | override: 5 | - file_option: java_package_prefix 6 | value: com.example.imports 7 | plugins: 8 | - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion 9 | out: build/generated/test-sources/bufgen 10 | inputs: 11 | - directory: src/test/resources/proto 12 | -------------------------------------------------------------------------------- /src/test/resources/proto/buf.gen.noimports.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | disable: 5 | - file_option: java_package 6 | module: buf.build/bufbuild/protovalidate 7 | override: 8 | - file_option: java_package_prefix 9 | value: com.example.noimports 10 | plugins: 11 | - remote: buf.build/protocolbuffers/java:$protocJavaPluginVersion 12 | out: build/generated/test-sources/bufgen 13 | inputs: 14 | - directory: src/main/resources 15 | - directory: src/test/resources/proto 16 | -------------------------------------------------------------------------------- /src/test/resources/proto/validationtest/custom_rules.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package validationtest; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | message FieldExpressionRepeatedMessage { 22 | repeated Msg val = 1 [(buf.validate.field).cel = { 23 | id: "field_expression.repeated.message" 24 | message: "test message field_expression.repeated.message" 25 | expression: "this.all(e, e.a == 1)" 26 | }]; 27 | message Msg { 28 | int32 a = 1; 29 | } 30 | } 31 | 32 | message FieldExpressionRepeatedMessageItems { 33 | repeated Msg val = 1 [(buf.validate.field).repeated.items.cel = { 34 | id: "field_expression.repeated.message.items" 35 | message: "test message field_expression.repeated.message.items" 36 | expression: "this.a == 1" 37 | }]; 38 | message Msg { 39 | int32 a = 1; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/proto/validationtest/import_test.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package validationtest; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | message ExampleImportedMessage { 22 | string hex_string = 1 [(buf.validate.field).string.pattern = "^[0-9a-fA-F]+$"]; 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/proto/validationtest/predefined.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto2"; 16 | 17 | package validationtest; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | extend buf.validate.StringRules { 22 | optional bool is_ident = 1161 [(buf.validate.predefined).cel = { 23 | id: "string.is_ident" 24 | expression: "(rule && !this.matches('^[a-z0-9]{1,9}$')) ? 'invalid identifier' : ''" 25 | }]; 26 | } 27 | 28 | message ExamplePredefinedFieldRules { 29 | optional string ident_field = 1 [(buf.validate.field).string.(is_ident) = true]; 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/proto/validationtest/required.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto2"; 16 | 17 | package validationtest; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | message ExampleRequiredFieldRules { 22 | required string regex_string_field = 1 [(buf.validate.field).string.pattern = "^[a-z0-9]{1,9}$"]; 23 | optional string unconstrained = 2; 24 | } 25 | -------------------------------------------------------------------------------- /src/test/resources/proto/validationtest/validationtest.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package validationtest; 18 | 19 | import "buf/validate/validate.proto"; 20 | import "validationtest/import_test.proto"; 21 | 22 | message ExampleFieldRules { 23 | string regex_string_field = 1 [(buf.validate.field).string.pattern = "^[a-z0-9]{1,9}$"]; 24 | string unconstrained = 2; 25 | } 26 | 27 | message ExampleOneofRules { 28 | // contact_info is the user's contact information 29 | oneof contact_info { 30 | // required ensures that exactly one field in oneof is set. Without this 31 | // option, at most one of email and phone_number is set. 32 | option (buf.validate.oneof).required = true; 33 | // email is the user's email 34 | string email = 1; 35 | // phone_number is the user's phone number. 36 | string phone_number = 2; 37 | } 38 | oneof unconstrained { 39 | string field3 = 3; 40 | string field4 = 4; 41 | } 42 | } 43 | 44 | message ExampleMessageRules { 45 | option (buf.validate.message).cel = { 46 | id: "secondary_email_depends_on_primary" 47 | expression: 48 | "has(this.secondary_email) && !has(this.primary_email)" 49 | "? 'cannot set a secondary email without setting a primary one'" 50 | ": ''" 51 | }; 52 | string primary_email = 1; 53 | string secondary_email = 2; 54 | } 55 | 56 | message FieldExpressionMultiple { 57 | string val = 1 [ 58 | (buf.validate.field).string.max_len = 5, 59 | (buf.validate.field).string.pattern = "^[a-z0-9]$" 60 | ]; 61 | } 62 | 63 | message FieldExpressionMapInt32 { 64 | map val = 1 [(buf.validate.field).cel = { 65 | id: "field_expression.map.int32" 66 | message: "all map values must equal 1" 67 | expression: "this.all(k, this[k] == 1)" 68 | }]; 69 | } 70 | 71 | message ExampleImportMessage { 72 | option (buf.validate.message) = { 73 | cel: { 74 | id: "imported_submessage_must_not_be_null" 75 | expression: "this.imported_submessage != null" 76 | } 77 | cel: { 78 | id: "hex_string_must_not_be_empty" 79 | expression: "this.imported_submessage.hex_string != ''" 80 | } 81 | }; 82 | ExampleImportedMessage imported_submessage = 1; 83 | } 84 | 85 | message ExampleImportMessageFieldRule { 86 | ExampleImportMessage message_with_import = 1 [ 87 | (buf.validate.field).cel = { 88 | id: "field_must_not_be_null" 89 | expression: "this.imported_submessage != null" 90 | }, 91 | (buf.validate.field).cel = { 92 | id: "field_string_must_not_be_empty" 93 | expression: "this.imported_submessage.hex_string != ''" 94 | } 95 | ]; 96 | } 97 | 98 | message ExampleImportMessageInMap { 99 | option (buf.validate.message) = { 100 | cel: { 101 | id: "imported_submessage_must_not_be_null" 102 | expression: "this.imported_submessage[0] != null" 103 | } 104 | cel: { 105 | id: "hex_string_must_not_be_empty" 106 | expression: "this.imported_submessage[0].hex_string != ''" 107 | } 108 | }; 109 | map imported_submessage = 1; 110 | } 111 | 112 | message ExampleImportMessageInMapFieldRule { 113 | ExampleImportMessageInMap message_with_import = 1 [ 114 | (buf.validate.field).cel = { 115 | id: "field_must_not_be_null" 116 | expression: "this.imported_submessage[0] != null" 117 | }, 118 | (buf.validate.field).cel = { 119 | id: "field_string_must_not_be_empty" 120 | expression: "this.imported_submessage[0].hex_string != ''" 121 | } 122 | ]; 123 | } 124 | -------------------------------------------------------------------------------- /src/test/resources/testdata/string_ext_supplemental.textproto: -------------------------------------------------------------------------------- 1 | # proto-file: ../../../proto/cel/expr/conformance/test/simple.proto 2 | # proto-message: cel.expr.conformance.test.SimpleTestFile 3 | 4 | # Ideally these tests should be in the cel-spec conformance test suite. 5 | # Until they are added, we can use this to test for additional functionality 6 | # listed in the spec. 7 | 8 | name: "string_ext_supplemental" 9 | description: "Supplemental tests for the strings extension library." 10 | section: { 11 | name: "format" 12 | test: { 13 | name: "bytes support for string with invalid utf-8 encoding" 14 | expr: '"%s".format([b"\\xF0abc\\x8C\\xF0xyz"])' 15 | value: { 16 | string_value: '\ufffdabc\ufffdxyz', 17 | } 18 | } 19 | test: { 20 | name: "bytes support for string with only invalid utf-8 sequences" 21 | expr: '"%s".format([b"\\xF0\\x8C\\xF0"])' 22 | value: { 23 | string_value: '\ufffd', 24 | } 25 | } 26 | } 27 | --------------------------------------------------------------------------------