├── .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 |
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 | [][buf]
2 |
3 | # protovalidate-java
4 |
5 | [](https://github.com/bufbuild/protovalidate-java/actions/workflows/ci.yaml)
6 | [](https://github.com/bufbuild/protovalidate-java/actions/workflows/conformance.yaml)
7 | [][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 extends Descriptors.FileDescriptor> 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 extends Descriptors.FileDescriptor> 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