├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── github-tag-action │ │ └── entrypoint.sh ├── buf-logo.svg ├── dependabot.yml └── workflows │ ├── add-to-project.yaml │ ├── buf.yaml │ ├── ci.yaml │ ├── conformance.yaml │ ├── emergency-review-bypass.yaml │ ├── notify-approval-bypass.yaml │ └── pr-hygiene.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── any.go ├── ast.go ├── ast_test.go ├── base.go ├── buf.gen.yaml ├── buf.lock ├── buf.yaml ├── builder.go ├── builder_test.go ├── cache.go ├── cache_test.go ├── cel.go ├── cel ├── library.go ├── library_test.go ├── lookups.go └── lookups_test.go ├── compilation_error.go ├── compile.go ├── compile_test.go ├── conformance └── expected_failures.yaml ├── enum.go ├── error_utils.go ├── error_utils_test.go ├── evaluator.go ├── field.go ├── filter.go ├── go.mod ├── go.sum ├── internal ├── cmd │ └── protovalidate-conformance-go │ │ ├── README.md │ │ └── main.go └── gen │ ├── buf │ └── validate │ │ └── conformance │ │ ├── cases │ │ ├── bool.pb.go │ │ ├── bool_protoopaque.pb.go │ │ ├── bytes.pb.go │ │ ├── bytes_protoopaque.pb.go │ │ ├── custom_rules │ │ │ ├── custom_rules.pb.go │ │ │ └── custom_rules_protoopaque.pb.go │ │ ├── enums.pb.go │ │ ├── enums_protoopaque.pb.go │ │ ├── filename-with-dash.pb.go │ │ ├── filename-with-dash_protoopaque.pb.go │ │ ├── ignore_empty_proto2.pb.go │ │ ├── ignore_empty_proto2_protoopaque.pb.go │ │ ├── ignore_empty_proto3.pb.go │ │ ├── ignore_empty_proto3_protoopaque.pb.go │ │ ├── ignore_empty_proto_editions.pb.go │ │ ├── ignore_empty_proto_editions_protoopaque.pb.go │ │ ├── ignore_proto2.pb.go │ │ ├── ignore_proto2_protoopaque.pb.go │ │ ├── ignore_proto3.pb.go │ │ ├── ignore_proto3_protoopaque.pb.go │ │ ├── ignore_proto_editions.pb.go │ │ ├── ignore_proto_editions_protoopaque.pb.go │ │ ├── kitchen_sink.pb.go │ │ ├── kitchen_sink_protoopaque.pb.go │ │ ├── library.pb.go │ │ ├── library_protoopaque.pb.go │ │ ├── maps.pb.go │ │ ├── maps_protoopaque.pb.go │ │ ├── messages.pb.go │ │ ├── messages_protoopaque.pb.go │ │ ├── numbers.pb.go │ │ ├── numbers_protoopaque.pb.go │ │ ├── oneofs.pb.go │ │ ├── oneofs_protoopaque.pb.go │ │ ├── other_package │ │ │ ├── embed.pb.go │ │ │ └── embed_protoopaque.pb.go │ │ ├── predefined_rules_proto2.pb.go │ │ ├── predefined_rules_proto2_protoopaque.pb.go │ │ ├── predefined_rules_proto3.pb.go │ │ ├── predefined_rules_proto3_protoopaque.pb.go │ │ ├── predefined_rules_proto_editions.pb.go │ │ ├── predefined_rules_proto_editions_protoopaque.pb.go │ │ ├── repeated.pb.go │ │ ├── repeated_protoopaque.pb.go │ │ ├── required_field_proto2.pb.go │ │ ├── required_field_proto2_protoopaque.pb.go │ │ ├── required_field_proto3.pb.go │ │ ├── required_field_proto3_protoopaque.pb.go │ │ ├── required_field_proto_editions.pb.go │ │ ├── required_field_proto_editions_protoopaque.pb.go │ │ ├── strings.pb.go │ │ ├── strings_protoopaque.pb.go │ │ ├── subdirectory │ │ │ ├── in_subdirectory.pb.go │ │ │ └── in_subdirectory_protoopaque.pb.go │ │ ├── wkt_any.pb.go │ │ ├── wkt_any_protoopaque.pb.go │ │ ├── wkt_duration.pb.go │ │ ├── wkt_duration_protoopaque.pb.go │ │ ├── wkt_nested.pb.go │ │ ├── wkt_nested_protoopaque.pb.go │ │ ├── wkt_timestamp.pb.go │ │ ├── wkt_timestamp_protoopaque.pb.go │ │ ├── wkt_wrappers.pb.go │ │ ├── wkt_wrappers_protoopaque.pb.go │ │ └── yet_another_package │ │ │ ├── embed2.pb.go │ │ │ └── embed2_protoopaque.pb.go │ │ └── harness │ │ ├── harness.pb.go │ │ ├── harness_protoopaque.pb.go │ │ ├── results.pb.go │ │ └── results_protoopaque.pb.go │ └── tests │ └── example │ └── v1 │ ├── compile.pb.go │ ├── compile_protoopaque.pb.go │ ├── example.pb.go │ ├── example_protoopaque.pb.go │ ├── filter.pb.go │ ├── filter_protoopaque.pb.go │ ├── predefined.pb.go │ ├── predefined_protoopaque.pb.go │ ├── validations.pb.go │ └── validations_protoopaque.pb.go ├── lookups.go ├── lookups_test.go ├── map.go ├── map_test.go ├── message.go ├── oneof.go ├── option.go ├── program.go ├── program_test.go ├── proto └── tests │ └── example │ └── v1 │ ├── compile.proto │ ├── example.proto │ ├── filter.proto │ ├── predefined.proto │ └── validations.proto ├── repeated.go ├── resolve.go ├── resolve_test.go ├── runtime_error.go ├── validation_error.go ├── validator.go ├── validator_bench_test.go ├── validator_example_test.go ├── validator_test.go ├── value.go ├── variable.go ├── variable_test.go └── violation.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go text eol=lf 2 | internal/gen/**/* linguist-generated=true 3 | -------------------------------------------------------------------------------- /.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/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Firstly, we want to say thank you for considering contributing 4 | to `protovalidate`. We genuinely appreciate your help. This document aims to 5 | provide some guidelines to make your contribution process straightforward and 6 | meaningful. 7 | 8 | ## Code of Conduct 9 | 10 | We pledge to maintain a welcoming and inclusive community. Please read 11 | our [Code of Conduct](../CODE_OF_CONDUCT.md) before participating. 12 | 13 | ## How Can I Contribute? 14 | 15 | ### Reporting Bugs 16 | 17 | Bugs are tracked as GitHub issues. If you discover a problem 18 | with `protovalidate`, we want to hear about it. Here's how you can report a bug: 19 | 20 | 1. __Ensure the bug was not already reported__: Before creating a new issue, 21 | please do a search 22 | in [issues](https://github.com/bufbuild/protovalidate/issues) to see if 23 | the problem has already been reported. If it has and the issue is still open, 24 | add a comment to the existing issue instead of opening a new one. 25 | 26 | 2. __Check if the issue is fixed__: Try to reproduce the issue using the 27 | latest `main` branch to see if the problem has already been fixed. If fixed, 28 | that's great! 29 | 30 | 3. __Open new issue__: If the issue has not been reported and has not been 31 | fixed, then we encourage you to [open a new issue][file-bug]. 32 | 33 | Remember to detail the steps to reproduce the issue. This information is 34 | invaluable in helping us fix the issue. 35 | 36 | Once you've filled in the template, hit "Submit new issue", and we will take 37 | care of the rest. We appreciate your contribution to making `protovalidate` 38 | better! 39 | 40 | ### Suggesting Enhancements 41 | 42 | We welcome ideas for enhancements and new features to improve `protovalidate`. 43 | If you have an idea you'd like to share, if you want to expand language 44 | support, 45 | please read [the section below](#language-support-requirements) first. 46 | 47 | 1. __Check if the enhancement is already suggested__: Before creating a new 48 | issue, please do a search 49 | in [issues](https://github.com/bufbuild/protovalidate/issues) to see if 50 | the idea or enhancement has already been suggested. If it has and the issue 51 | is still open, add a comment to the existing issue instead of opening a new 52 | one. 53 | 54 | 2. __Open a new issue__: If your enhancement hasn't been suggested before, 55 | please [create a new issue][file-feature-request]. 56 | 57 | 3. __Discussion__: Once you've submitted the issue, maintainers or other 58 | community members might jump in to discuss the enhancement. Be prepared to 59 | provide more context or insights about your suggestion. 60 | 61 | Remember, the goal of suggesting an enhancement is to improve `protovalidate` 62 | for everyone. Every suggestion is valued, and we thank you in advance for your 63 | contribution. 64 | 65 | ### Pull Requests 66 | 67 | For changes, improvements, or fixes, please create a pull request. Make sure 68 | your PR is up-to-date with the main branch. Please write clear and concise 69 | commit messages to help us understand and review your PR. 70 | 71 | ### Minimizing Performance Regression 72 | 73 | Performance and efficient resource management are critical aspects 74 | of `protovalidate`. CEL, being non-Turing complete, provides production safety 75 | controls to limit execution time, helping to prevent excessive resource 76 | consumption during evaluation. Here are some guidelines for effectively managing 77 | resource constraints and minimizing performance regressions: 78 | 79 | 1. __Understanding Resource Constraints__: CEL has resource constraint features 80 | which provide feedback about expression complexity. These are designed to 81 | prevent CEL evaluation from consuming excessive resources. One key element is 82 | the concept of a _cost unit_, an independent measure used for tracking CPU 83 | utilization regardless of system load or hardware. The cost is deterministic, 84 | meaning for any CEL expression and input data, the evaluation cost will 85 | remain the same. 86 | 2. __Cost Units and Operations__: Many of CEL's operations have fixed costs. 87 | Simple operations, such as comparisons (e.g. `<`), have a cost of 1, while 88 | some, like list literal declarations, have a higher fixed cost of 40 cost 89 | units. Functions implemented in native code approximate cost based on time 90 | complexity. For instance, regular expression operations like `match` 91 | and `find` use an approximated cost 92 | of `length(regexString)*length(inputString)`, reflecting the worst-case time 93 | complexity of the operation. 94 | 3. __Testing the Overall Cost__: You can use the CEL library to test the overall 95 | cost of an expression. This can help predict the resources required to 96 | evaluate an expression and prevent operations that would consume excessive 97 | resources. 98 | 4. __Benchmark and Profile__: Benchmark your changes against the current `main` 99 | branch to evaluate the performance impact. If a performance regression is 100 | suspected, profile the code to pinpoint the bottleneck. 101 | 5. __Optimize__: Always look for ways to optimize your changes without 102 | compromising readability, maintainability, or correctness of your code. 103 | 6. __Discuss__: If your changes might cause a performance regression or resource 104 | constraint, but you believe they're still beneficial, discuss this in the 105 | pull request. Explain why you think the performance regression or resource 106 | constraint might be acceptable. 107 | 108 | By keeping performance and resource management in mind throughout the 109 | development process, we can ensure `protovalidate` remains efficient and 110 | responsive, even as we add new features and fix bugs. 111 | 112 | ## Questions? 113 | 114 | If you have any questions, please don't hesitate to create an issue, and we'll 115 | answer as soon as possible. If your question is regarding a specific issue or 116 | pull request, please link it in your comment. 117 | 118 | ## Thank You 119 | 120 | Again, we appreciate your help and time, and we are excited to see your 121 | contributions! 122 | 123 | Remember, you can reach out to us at any time, and we're looking forward to 124 | working together to make `protovalidate` the best it can be. 125 | 126 | [file-bug]: https://github.com/bufbuild/protovalidate/issues/new?assignees=&labels=Bug&template=bug_report.md&title=%5BBUG%5D 127 | 128 | [file-feature-request]: https://github.com/bufbuild/protovalidate/issues/new?assignees=&labels=Feature&template=feature_request.md&title=%5BFeature+Request%5D 129 | 130 | [cel-spec]: https://github.com/google/cel-spec 131 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: Bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | 13 | ## Steps to Reproduce 14 | 20 | 21 | ## Expected Behavior 22 | 23 | 24 | 25 | ## Actual Behavior 26 | 27 | 28 | 29 | ## Screenshots/Logs 30 | 31 | 32 | 33 | ## Environment 34 | 35 | - **Operating System**: 36 | - **Version**: 37 | - **Compiler/Toolchain**: 38 | - **Protobuf Compiler & Version**: 39 | - **Protovalidate Version**: 40 | 41 | ## Possible Solution 42 | 43 | 44 | ## Additional Context 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature Request]" 5 | labels: Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Feature description:** 11 | 12 | 13 | **Problem it solves or use case:** 14 | 15 | 16 | **Proposed implementation or solution:** 17 | 18 | 19 | **Contribution:** 20 | 21 | 22 | **Examples or references:** 23 | 24 | 25 | **Additional context:** 26 | 27 | -------------------------------------------------------------------------------- /.github/buf-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | github-actions: 9 | patterns: ["*"] 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | groups: 15 | go: 16 | patterns: ["*"] 17 | -------------------------------------------------------------------------------- /.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/buf.yaml: -------------------------------------------------------------------------------- 1 | name: Buf 2 | on: 3 | push: 4 | branches: [ main ] 5 | tags: [ 'v*' ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: { } # support manual runs 9 | 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | 14 | jobs: 15 | validate-protos: 16 | name: Validate protos 17 | if: ${{ github.event_name == 'pull_request'}} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 2 24 | - name: Buf CI 25 | uses: bufbuild/buf-action@v1 26 | with: 27 | github_token: ${{ github.token }} 28 | token: ${{ secrets.BUF_TOKEN }} 29 | format: true 30 | lint: true 31 | push: false 32 | archive: false 33 | - name: Check Generate 34 | run: make checkgenerate 35 | -------------------------------------------------------------------------------- /.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 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | go: 17 | name: Go 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | go-version: 22 | - 1.23.x 23 | - 1.24.x 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 1 29 | - name: Install go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | - name: Test 34 | run: make test 35 | - name: Lint 36 | # Often, lint guidelines depend on the Go version. To prevent 37 | # conflicting guidance, run only on the most recent supported version. 38 | if: matrix.go-version == '1.24.x' 39 | run: make lint-go 40 | -------------------------------------------------------------------------------- /.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 | go: 15 | name: Go 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | go-version: [ 'stable', 'oldstable' ] 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 1 26 | - name: Install go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | - name: Test conformance 31 | run: make conformance 32 | -------------------------------------------------------------------------------- /.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@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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.tmp/ 2 | *.pprof 3 | *.svg 4 | cover.out 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - cyclop # covered by gocyclo 6 | - depguard # we can manage dependencies strictly if the need arises in the future 7 | - err113 # internal error causes may be dynamic 8 | - exhaustruct # don't _always_ need to exhaustively create struct 9 | - funcorder # consider enabling in the future 10 | - funlen # rely on code review to limit function length 11 | - gocognit # dubious "cognitive overhead" quantification 12 | - gomoddirectives # we use go modules replacements intentionally 13 | - gomodguard # not compatible with go workspaces 14 | - ireturn # "accept interfaces, return structs" isn't ironclad 15 | - lll # don't want hard limits for line length 16 | - maintidx # covered by gocyclo 17 | - mnd # some unnamed constants are okay 18 | - nlreturn # generous whitespace violates house style 19 | - nonamedreturns # usage of named returns should be selective 20 | - testpackage # internal tests are fine 21 | - wrapcheck # don't _always_ need to wrap errors 22 | - wsl # over-generous whitespace violates house style 23 | settings: 24 | errcheck: 25 | check-type-assertions: true 26 | forbidigo: 27 | forbid: 28 | - pattern: ^fmt\.Print 29 | - pattern: ^log\. 30 | - pattern: ^print$ 31 | - pattern: ^println$ 32 | - pattern: ^panic$ 33 | godox: 34 | # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for 35 | # temporary hacks, and use godox to prevent committing them. 36 | keywords: 37 | - FIXME 38 | varnamelen: 39 | ignore-decls: 40 | - T any 41 | - i int 42 | - wg sync.WaitGroup 43 | - ok bool 44 | - w io.Writer 45 | exclusions: 46 | generated: lax 47 | presets: 48 | - comments 49 | - common-false-positives 50 | - legacy 51 | - std-error-handling 52 | rules: 53 | # Loosen requirements on conformance executor 54 | - linters: 55 | - errorlint 56 | - forbidigo 57 | path: internal/cmd/ 58 | # Loosen requirements on tests 59 | - linters: 60 | - funlen 61 | - gosec 62 | - gosmopolitan 63 | - unparam 64 | - varnamelen 65 | path: _test.go 66 | - linters: 67 | # setting up custom functions/overloads appears duplicative (false positive) 68 | - dupl 69 | # Types are checked internally within CEL. There are bigger issues if its 70 | # type analysis is wrong 71 | - forcetypeassert 72 | path: cel/library.go 73 | # static unexported lookup tables 74 | - linters: 75 | - gochecknoglobals 76 | path: lookups.go 77 | - linters: 78 | # uses deprecated fields on protoimpl.ExtensionInfo but its the only way 79 | - staticcheck 80 | path: resolver/resolver.go 81 | # We allow a global validator. 82 | - linters: 83 | - gochecknoglobals 84 | path: validator.go 85 | # Library code uses for loops to implement parsing that often don't have bodies. 86 | # Unfortunately, revive doesn't detect comments within these empty for loops. 87 | - linters: 88 | - revive 89 | text: "empty-block" 90 | path: cel/library.go 91 | ### BEGIN Temporary exclusions from golangci-lint upgrade. 92 | # Will remove in a future PR. 93 | - linters: 94 | - staticcheck 95 | text: "QF1001:" # could apply De Morgan's law 96 | ### END Temporary exclusions 97 | paths: 98 | - third_party$ 99 | - builtin$ 100 | - examples$ 101 | issues: 102 | max-same-issues: 0 103 | formatters: 104 | enable: 105 | - gci 106 | - gofmt 107 | exclusions: 108 | generated: lax 109 | paths: 110 | - third_party$ 111 | - builtin$ 112 | - examples$ 113 | -------------------------------------------------------------------------------- /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 | BIN := .tmp/bin 10 | COPYRIGHT_YEARS := 2023-2024 11 | LICENSE_IGNORE := -e internal/testdata/ 12 | # Set to use a different compiler. For example, `GO=go1.18rc1 make test`. 13 | GO ?= go 14 | ARGS ?= --strict_message --strict_error 15 | GOLANGCI_LINT_VERSION ?= v2.1.2 16 | # Set to use a different version of protovalidate-conformance. 17 | # Should be kept in sync with the version referenced in buf.yaml and 18 | # 'buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go' in go.mod. 19 | CONFORMANCE_VERSION ?= v1.0.0-rc.1 20 | 21 | .PHONY: help 22 | help: ## Describe useful make targets 23 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}' 24 | 25 | .PHONY: all 26 | all: generate test conformance lint ## Generate and run all tests and lint (default) 27 | 28 | .PHONY: clean 29 | clean: ## Delete intermediate build artifacts 30 | @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs 31 | git clean -Xdf 32 | 33 | .PHONY: test 34 | test: ## Run all unit tests 35 | $(GO) test -race -cover ./... 36 | 37 | .PHONY: lint 38 | lint: lint-proto lint-go ## Lint code and protos 39 | 40 | .PHONY: lint-go 41 | lint-go: $(BIN)/golangci-lint 42 | $(BIN)/golangci-lint run --modules-download-mode=readonly --timeout=3m0s ./... 43 | $(BIN)/golangci-lint fmt --diff 44 | 45 | .PHONY: lint-proto 46 | lint-proto: $(BIN)/buf 47 | $(BIN)/buf lint 48 | 49 | .PHONY: lint-fix 50 | lint-fix: 51 | $(BIN)/golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s ./... 52 | $(BIN)/golangci-lint fmt 53 | 54 | .PHONY: conformance 55 | conformance: $(BIN)/protovalidate-conformance protovalidate-conformance-go ## Run conformance tests 56 | $(BIN)/protovalidate-conformance $(ARGS) $(BIN)/protovalidate-conformance-go --expected_failures=conformance/expected_failures.yaml 57 | 58 | .PHONY: generate 59 | generate: generate-proto generate-license ## Regenerate code and license headers 60 | $(GO) mod tidy 61 | 62 | .PHONY: generate-proto 63 | generate-proto: $(BIN)/buf 64 | rm -rf internal/gen/*/ 65 | $(BIN)/buf generate buf.build/bufbuild/protovalidate-testing:$(CONFORMANCE_VERSION) 66 | $(BIN)/buf generate 67 | 68 | .PHONY: generate-license 69 | generate-license: $(BIN)/license-header 70 | @# We want to operate on a list of modified and new files, excluding 71 | @# deleted and ignored files. git-ls-files can't do this alone. comm -23 takes 72 | @# two files and prints the union, dropping lines common to both (-3) and 73 | @# those only in the second file (-2). We make one git-ls-files call for 74 | @# the modified, cached, and new (--others) files, and a second for the 75 | @# deleted files. 76 | comm -23 \ 77 | <(git ls-files --cached --modified --others --no-empty-directory --exclude-standard | sort -u | grep -v $(LICENSE_IGNORE) ) \ 78 | <(git ls-files --deleted | sort -u) | \ 79 | xargs $(BIN)/license-header \ 80 | --license-type apache \ 81 | --copyright-holder "Buf Technologies, Inc." \ 82 | --year-range "$(COPYRIGHT_YEARS)" 83 | 84 | .PHONY: checkgenerate 85 | checkgenerate: generate 86 | @# Used in CI to verify that `make generate` doesn't produce a diff. 87 | test -z "$$(git status --porcelain | tee /dev/stderr)" 88 | 89 | .PHONY: upgrade-go 90 | upgrade-go: 91 | $(GO) get -u -t ./... && $(GO) mod tidy -v 92 | 93 | $(BIN): 94 | @mkdir -p $(BIN) 95 | 96 | $(BIN)/buf: $(BIN) Makefile 97 | GOBIN=$(abspath $(@D)) $(GO) install github.com/bufbuild/buf/cmd/buf@latest 98 | 99 | $(BIN)/license-header: $(BIN) Makefile 100 | GOBIN=$(abspath $(@D)) $(GO) install \ 101 | github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@latest 102 | 103 | $(BIN)/golangci-lint: $(BIN) Makefile 104 | GOBIN=$(abspath $(@D)) $(GO) install \ 105 | github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) 106 | 107 | $(BIN)/protovalidate-conformance: $(BIN) Makefile 108 | GOBIN=$(abspath $(BIN)) $(GO) install \ 109 | github.com/bufbuild/protovalidate/tools/protovalidate-conformance@$(CONFORMANCE_VERSION) 110 | 111 | .PHONY: protovalidate-conformance-go 112 | protovalidate-conformance-go: $(BIN) 113 | GOBIN=$(abspath $(BIN)) $(GO) install ./internal/cmd/protovalidate-conformance-go 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![The Buf logo](.github/buf-logo.svg)][buf] 2 | 3 | # protovalidate-go 4 | 5 | [![CI](https://github.com/bufbuild/protovalidate-go/actions/workflows/ci.yaml/badge.svg)](https://github.com/bufbuild/protovalidate-go/actions/workflows/ci.yaml) 6 | [![Conformance](https://github.com/bufbuild/protovalidate-go/actions/workflows/conformance.yaml/badge.svg)](https://github.com/bufbuild/protovalidate-go/actions/workflows/conformance.yaml) 7 | [![Report Card](https://goreportcard.com/badge/buf.build/go/protovalidate)](https://goreportcard.com/report/buf.build/go/protovalidate) 8 | [![GoDoc](https://pkg.go.dev/badge/buf.build/go/protovalidate.svg)](https://pkg.go.dev/buf.build/go/protovalidate) 9 | [![BSR](https://img.shields.io/badge/BSR-Module-0C65EC)][buf-mod] 10 | 11 | [Protovalidate][protovalidate] 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], the only widely used validation library for Protobuf. 12 | 13 | With Protovalidate, you can annotate your Protobuf messages with both standard and custom validation rules: 14 | 15 | ```protobuf 16 | syntax = "proto3"; 17 | 18 | package banking.v1; 19 | 20 | import "buf/validate/validate.proto"; 21 | 22 | message MoneyTransfer { 23 | string to_account_id = 1 [ 24 | // Standard rule: `to_account_id` must be a UUID. 25 | (buf.validate.field).string.uuid = true 26 | ]; 27 | 28 | string from_account_id = 2 [ 29 | // Standard rule: `from_account_id` must be a UUID. 30 | (buf.validate.field).string.uuid = true 31 | ]; 32 | 33 | // Custom rule: `to_account_id` and `from_account_id` can't be the same. 34 | option (buf.validate.message).cel = { 35 | id: "to_account_id.not.from_account_id" 36 | message: "to_account_id and from_account_id should not be the same value" 37 | expression: "this.to_account_id != this.from_account_id" 38 | }; 39 | } 40 | ``` 41 | 42 | Once you've added `protovalidate-go` to your project, validation is idiomatic Go: 43 | 44 | ```go 45 | if err = protovalidate.Validate(moneyTransfer); err != nil { 46 | // Handle failure. 47 | } 48 | ``` 49 | 50 | ## Installation 51 | 52 | > [!TIP] 53 | > The easiest way to get started with Protovalidate for RPC APIs are the quickstarts in Buf's documentation. They're available for both [Connect][connect-go] and [gRPC][grpc-go]. 54 | 55 | To install the package, use `go get` from within your Go module: 56 | 57 | ```shell 58 | go get buf.build/go/protovalidate 59 | ``` 60 | 61 | ## Documentation 62 | 63 | Comprehensive documentation for Protovalidate is available in [Buf's documentation library][protovalidate]. 64 | 65 | Highlights for Go developers include: 66 | 67 | * The [developer quickstart][quickstart] 68 | * Comprehensive RPC quickstarts for [Connect][connect-go] and [gRPC][grpc-go] 69 | * A [migration guide for protoc-gen-validate][migration-guide] users 70 | 71 | API documentation for Go is available on [pkg.go.dev][pkg-go]. 72 | 73 | ## Additional Languages and Repositories 74 | 75 | Protovalidate isn't just for Go! You might be interested in sibling repositories for other languages: 76 | 77 | - [`protovalidate-java`][pv-java] (Java) 78 | - [`protovalidate-python`][pv-python] (Python) 79 | - [`protovalidate-cc`][pv-cc] (C++) 80 | - [`protovalidate-es`][pv-es] (TypeScript and JavaScript) 81 | 82 | Additionally, [protovalidate's core repository](https://github.com/bufbuild/protovalidate) provides: 83 | 84 | - [Protovalidate's Protobuf API][validate-proto] 85 | - [Conformance testing utilities][conformance] for acceptance testing of `protovalidate` implementations 86 | 87 | ## Contribution 88 | 89 | We genuinely appreciate any help! If you'd like to contribute, check out these resources: 90 | 91 | - [Contributing Guidelines][contributing]: Guidelines to make your contribution process straightforward and meaningful 92 | - [Conformance testing utilities](https://github.com/bufbuild/protovalidate/tree/main/docs/conformance.md): Utilities providing acceptance testing of `protovalidate` implementations 93 | - [Go conformance executor][conformance-executable]: Conformance testing executor for `protovalidate-go` 94 | 95 | ## Related Sites 96 | 97 | - [Buf][buf]: Enterprise-grade Kafka and gRPC for the modern age 98 | - [Common Expression Language (CEL)][cel]: The open-source technology at the core of Protovalidate 99 | 100 | ## Legal 101 | 102 | Offered under the [Apache 2 license][license]. 103 | 104 | [buf]: https://buf.build 105 | [cel]: https://cel.dev 106 | 107 | [pv-go]: https://github.com/bufbuild/protovalidate-go 108 | [pv-java]: https://github.com/bufbuild/protovalidate-java 109 | [pv-python]: https://github.com/bufbuild/protovalidate-python 110 | [pv-cc]: https://github.com/bufbuild/protovalidate-cc 111 | [pv-es]: https://github.com/bufbuild/protovalidate-es 112 | 113 | [buf-mod]: https://buf.build/bufbuild/protovalidate 114 | [license]: LICENSE 115 | [contributing]: .github/CONTRIBUTING.md 116 | 117 | [protoc-gen-validate]: https://github.com/bufbuild/protoc-gen-validate 118 | 119 | [protovalidate]: https://buf.build/docs/protovalidate/ 120 | [quickstart]: https://buf.build/docs/protovalidate/quickstart/ 121 | [connect-go]: https://buf.build/docs/protovalidate/quickstart/connect-go/ 122 | [grpc-go]: https://buf.build/docs/protovalidate/quickstart/grpc-go/ 123 | [grpc-java]: https://buf.build/docs/protovalidate/quickstart/grpc-java/ 124 | [grpc-python]: https://buf.build/docs/protovalidate/quickstart/grpc-python/ 125 | [migration-guide]: https://buf.build/docs/migration-guides/migrate-from-protoc-gen-validate/ 126 | [conformance-executable]: ./internal/cmd/protovalidate-conformance-go/README.md 127 | [pkg-go]: https://pkg.go.dev/buf.build/go/protovalidate 128 | 129 | [validate-proto]: https://buf.build/bufbuild/protovalidate/docs/main:buf.validate 130 | [conformance]: https://github.com/bufbuild/protovalidate/blob/main/docs/conformance.md 131 | [examples]: https://github.com/bufbuild/protovalidate/tree/main/examples 132 | [migrate]: https://buf.build/docs/migration-guides/migrate-from-protoc-gen-validate/ 133 | -------------------------------------------------------------------------------- /any.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | //nolint:gochecknoglobals 24 | var ( 25 | anyRuleDescriptor = (&validate.FieldRules{}).ProtoReflect().Descriptor().Fields().ByName("any") 26 | anyInRuleDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("in") 27 | anyInRulePath = &validate.FieldPath{ 28 | Elements: []*validate.FieldPathElement{ 29 | fieldPathElement(anyRuleDescriptor), 30 | fieldPathElement(anyInRuleDescriptor), 31 | }, 32 | } 33 | anyNotInDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("not_in") 34 | anyNotInRulePath = &validate.FieldPath{ 35 | Elements: []*validate.FieldPathElement{ 36 | fieldPathElement(anyRuleDescriptor), 37 | fieldPathElement(anyNotInDescriptor), 38 | }, 39 | } 40 | ) 41 | 42 | // anyPB is a specialized evaluator for applying validate.AnyRules to an 43 | // anypb.Any message. This is handled outside CEL which attempts to 44 | // hydrate anyPB's within an expression, breaking evaluation if the type is 45 | // unknown at runtime. 46 | type anyPB struct { 47 | base base 48 | 49 | // TypeURLDescriptor is the descriptor for the TypeURL field 50 | TypeURLDescriptor protoreflect.FieldDescriptor 51 | // In specifies which type URLs the value may possess 52 | In map[string]struct{} 53 | // NotIn specifies which type URLs the value may not possess 54 | NotIn map[string]struct{} 55 | // InValue contains the original `in` rule value. 56 | InValue protoreflect.Value 57 | // NotInValue contains the original `not_in` rule value. 58 | NotInValue protoreflect.Value 59 | } 60 | 61 | func (a anyPB) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 62 | typeURL := val.Message().Get(a.TypeURLDescriptor).String() 63 | 64 | err := &ValidationError{} 65 | if len(a.In) > 0 { 66 | if _, ok := a.In[typeURL]; !ok { 67 | err.Violations = append(err.Violations, &Violation{ 68 | Proto: &validate.Violation{ 69 | Field: a.base.fieldPath(), 70 | Rule: a.base.rulePath(anyInRulePath), 71 | RuleId: proto.String("any.in"), 72 | Message: proto.String("type URL must be in the allow list"), 73 | }, 74 | FieldValue: val, 75 | FieldDescriptor: a.base.Descriptor, 76 | RuleValue: a.InValue, 77 | RuleDescriptor: anyInRuleDescriptor, 78 | }) 79 | if cfg.failFast { 80 | return err 81 | } 82 | } 83 | } 84 | 85 | if len(a.NotIn) > 0 { 86 | if _, ok := a.NotIn[typeURL]; ok { 87 | err.Violations = append(err.Violations, &Violation{ 88 | Proto: &validate.Violation{ 89 | Field: a.base.fieldPath(), 90 | Rule: a.base.rulePath(anyNotInRulePath), 91 | RuleId: proto.String("any.not_in"), 92 | Message: proto.String("type URL must not be in the block list"), 93 | }, 94 | FieldValue: val, 95 | FieldDescriptor: a.base.Descriptor, 96 | RuleValue: a.NotInValue, 97 | RuleDescriptor: anyNotInDescriptor, 98 | }) 99 | } 100 | } 101 | 102 | if len(err.Violations) > 0 { 103 | return err 104 | } 105 | return nil 106 | } 107 | 108 | func (a anyPB) Tautology() bool { 109 | return len(a.In) == 0 && len(a.NotIn) == 0 110 | } 111 | 112 | func stringsToSet(ss []string) map[string]struct{} { 113 | if len(ss) == 0 { 114 | return nil 115 | } 116 | set := make(map[string]struct{}, len(ss)) 117 | for _, s := range ss { 118 | set[s] = struct{}{} 119 | } 120 | return set 121 | } 122 | 123 | var _ evaluator = anyPB{} 124 | -------------------------------------------------------------------------------- /ast.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | "slices" 20 | 21 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 22 | pvcel "buf.build/go/protovalidate/cel" 23 | "github.com/google/cel-go/cel" 24 | "github.com/google/cel-go/interpreter" 25 | "google.golang.org/protobuf/reflect/protoreflect" 26 | ) 27 | 28 | // astSet represents a collection of compiledAST and their associated cel.Env. 29 | type astSet []compiledAST 30 | 31 | // Merge combines a set with another, producing a new ASTSet. 32 | func (set astSet) Merge(other astSet) astSet { 33 | out := make([]compiledAST, 0, len(set)+len(other)) 34 | out = append(out, set...) 35 | out = append(out, other...) 36 | return out 37 | } 38 | 39 | // ReduceResiduals generates a ProgramSet, performing a partial evaluation of 40 | // the ASTSet to optimize the expression. If the expression is optimized to 41 | // either a true or empty string constant result, no compiledProgram is 42 | // generated for it. The main usage of this is to elide tautological expressions 43 | // from the final result. 44 | func (set astSet) ReduceResiduals(opts ...cel.ProgramOption) (programSet, error) { 45 | residuals := make(astSet, 0, len(set)) 46 | options := append([]cel.ProgramOption{ 47 | cel.EvalOptions( 48 | cel.OptTrackState, 49 | cel.OptExhaustiveEval, 50 | cel.OptOptimize, 51 | cel.OptPartialEval, 52 | ), 53 | }, opts...) 54 | 55 | for _, ast := range set { 56 | options := slices.Clone(options) 57 | if ast.Value.IsValid() { 58 | options = append(options, cel.Globals(&variable{Name: "rule", Val: ast.Value.Interface()})) 59 | } 60 | program, err := ast.toProgram(ast.Env, options...) 61 | if err != nil { 62 | residuals = append(residuals, ast) 63 | continue 64 | } 65 | val, details, _ := program.Program.Eval(interpreter.EmptyActivation()) 66 | if val != nil { 67 | switch value := val.Value().(type) { 68 | case bool: 69 | if value { 70 | continue 71 | } 72 | case string: 73 | if value == "" { 74 | continue 75 | } 76 | } 77 | } 78 | residual, err := ast.Env.ResidualAst(ast.AST, details) 79 | if err != nil { 80 | residuals = append(residuals, ast) 81 | } else { 82 | residuals = append(residuals, compiledAST{ 83 | AST: residual, 84 | Env: ast.Env, 85 | Source: ast.Source, 86 | Path: ast.Path, 87 | Value: ast.Value, 88 | Descriptor: ast.Descriptor, 89 | }) 90 | } 91 | } 92 | 93 | return residuals.ToProgramSet(opts...) 94 | } 95 | 96 | // ToProgramSet generates a ProgramSet from the specified ASTs. 97 | func (set astSet) ToProgramSet(opts ...cel.ProgramOption) (out programSet, err error) { 98 | if l := len(set); l == 0 { 99 | return nil, nil 100 | } 101 | out = make(programSet, len(set)) 102 | for i, ast := range set { 103 | out[i], err = ast.toProgram(ast.Env, opts...) 104 | if err != nil { 105 | return nil, err 106 | } 107 | } 108 | return out, nil 109 | } 110 | 111 | // SetRuleValue sets the rule value for the programs in the ASTSet. 112 | func (set astSet) WithRuleValue( 113 | ruleValue protoreflect.Value, 114 | ruleDescriptor protoreflect.FieldDescriptor, 115 | ) (out astSet, err error) { 116 | out = slices.Clone(set) 117 | for i := range set { 118 | out[i].Env, err = out[i].Env.Extend( 119 | cel.Constant( 120 | "rule", 121 | pvcel.ProtoFieldToType(ruleDescriptor, true, false), 122 | pvcel.ProtoFieldToValue(ruleDescriptor, ruleValue, false), 123 | ), 124 | ) 125 | if err != nil { 126 | return nil, err 127 | } 128 | out[i].Value = ruleValue 129 | out[i].Descriptor = ruleDescriptor 130 | } 131 | return out, nil 132 | } 133 | 134 | type compiledAST struct { 135 | AST *cel.Ast 136 | Env *cel.Env 137 | Source *validate.Rule 138 | Path []*validate.FieldPathElement 139 | Value protoreflect.Value 140 | Descriptor protoreflect.FieldDescriptor 141 | } 142 | 143 | func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out compiledProgram, err error) { 144 | prog, err := env.Program(ast.AST, opts...) 145 | if err != nil { 146 | return out, &CompilationError{cause: fmt.Errorf("failed to compile program %s: %w", ast.Source.GetId(), err)} 147 | } 148 | return compiledProgram{ 149 | Program: prog, 150 | Source: ast.Source, 151 | Path: ast.Path, 152 | Value: ast.Value, 153 | Descriptor: ast.Descriptor, 154 | }, nil 155 | } 156 | -------------------------------------------------------------------------------- /ast_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | pvcel "buf.build/go/protovalidate/cel" 22 | "github.com/google/cel-go/cel" 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | "google.golang.org/protobuf/proto" 26 | ) 27 | 28 | func TestASTSet_Merge(t *testing.T) { 29 | t.Parallel() 30 | 31 | var set astSet 32 | other := astSet{ 33 | {AST: &cel.Ast{}}, 34 | {AST: &cel.Ast{}}, 35 | } 36 | merged := set.Merge(other) 37 | assert.Equal(t, other, merged) 38 | 39 | another := astSet{ 40 | {AST: &cel.Ast{}}, 41 | {AST: &cel.Ast{}}, 42 | {AST: &cel.Ast{}}, 43 | } 44 | merged = other.Merge(another) 45 | assert.Equal(t, other, merged[0:2]) 46 | assert.Equal(t, another, merged[2:]) 47 | } 48 | 49 | func TestASTSet_ToProgramSet(t *testing.T) { 50 | t.Parallel() 51 | 52 | env, err := cel.NewEnv(cel.Lib(pvcel.NewLibrary())) 53 | require.NoError(t, err) 54 | 55 | asts, err := compileASTs( 56 | expressions{ 57 | Rules: []*validate.Rule{ 58 | {Expression: proto.String("foo")}, 59 | }, 60 | }, 61 | env, 62 | cel.Variable("foo", cel.BoolType), 63 | ) 64 | require.NoError(t, err) 65 | assert.Len(t, asts, 1) 66 | set, err := asts.ToProgramSet() 67 | require.NoError(t, err) 68 | assert.Len(t, set, 1) 69 | assert.Equal(t, asts[0].Source, set[0].Source) 70 | 71 | empty := astSet{} 72 | set, err = empty.ToProgramSet() 73 | assert.Empty(t, set) 74 | require.NoError(t, err) 75 | } 76 | 77 | func TestASTSet_ReduceResiduals(t *testing.T) { 78 | t.Parallel() 79 | 80 | env, err := cel.NewEnv(cel.Lib(pvcel.NewLibrary())) 81 | require.NoError(t, err) 82 | 83 | asts, err := compileASTs( 84 | expressions{ 85 | Rules: []*validate.Rule{ 86 | {Expression: proto.String("foo")}, 87 | }, 88 | }, 89 | env, 90 | cel.Variable("foo", cel.BoolType), 91 | ) 92 | require.NoError(t, err) 93 | assert.Len(t, asts, 1) 94 | set, err := asts.ReduceResiduals(cel.Globals(&variable{Name: "foo", Val: true})) 95 | require.NoError(t, err) 96 | assert.Empty(t, set) 97 | } 98 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "slices" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | "google.golang.org/protobuf/reflect/protoreflect" 22 | ) 23 | 24 | // base is a common struct used by all field evaluators. It holds 25 | // some common information used across all field evaluators. 26 | type base struct { 27 | // Descriptor is the FieldDescriptor targeted by this evaluator, nor nil if 28 | // there is none. 29 | Descriptor protoreflect.FieldDescriptor 30 | 31 | // FieldPatht is the field path element that pertains to this evaluator, or 32 | // nil if there is none. 33 | FieldPathElement *validate.FieldPathElement 34 | 35 | // RulePrefix is a static prefix this evaluator should add to the rule path 36 | // of violations. 37 | RulePrefix *validate.FieldPath 38 | } 39 | 40 | func newBase(valEval *value) base { 41 | return base{ 42 | Descriptor: valEval.Descriptor, 43 | FieldPathElement: fieldPathElement(valEval.Descriptor), 44 | RulePrefix: valEval.NestedRule, 45 | } 46 | } 47 | 48 | func (b *base) fieldPath() *validate.FieldPath { 49 | if b.FieldPathElement == nil { 50 | return nil 51 | } 52 | return &validate.FieldPath{ 53 | Elements: []*validate.FieldPathElement{ 54 | b.FieldPathElement, 55 | }, 56 | } 57 | } 58 | 59 | func (b *base) rulePath(suffix *validate.FieldPath) *validate.FieldPath { 60 | return prefixRulePath(b.RulePrefix, suffix) 61 | } 62 | 63 | func prefixRulePath(prefix *validate.FieldPath, suffix *validate.FieldPath) *validate.FieldPath { 64 | if len(prefix.GetElements()) > 0 { 65 | return &validate.FieldPath{ 66 | Elements: slices.Concat(prefix.GetElements(), suffix.GetElements()), 67 | } 68 | } 69 | return suffix 70 | } 71 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | disable: 5 | - file_option: go_package 6 | module: buf.build/bufbuild/protovalidate 7 | override: 8 | - file_option: go_package_prefix 9 | value: buf.build/go/protovalidate/internal/gen 10 | plugins: 11 | - remote: buf.build/protocolbuffers/go:v1.36.6 12 | out: internal/gen 13 | opt: 14 | - paths=source_relative 15 | - default_api_level=API_HYBRID 16 | -------------------------------------------------------------------------------- /buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v2 3 | deps: 4 | - name: buf.build/bufbuild/protovalidate 5 | commit: 8976f5be98c146529b1cc15cd2012b60 6 | digest: b5:5d513af91a439d9e78cacac0c9455c7cb885a8737d30405d0b91974fe05276d19c07a876a51a107213a3d01b83ecc912996cdad4cddf7231f91379079cf7488d 7 | - name: buf.build/bufbuild/protovalidate-testing 8 | commit: d2080a16593140fab100cb64735f873a 9 | digest: b5:2a9179fe4391637210e19ba69bc38f3796450f7c231c69153f93dea3523e5e0cbb3f38eda2da90f58585cbfb52e6cf148d399e93e23a8855d5cbed7fcdd23b29 10 | - name: buf.build/envoyproxy/protoc-gen-validate 11 | commit: daf171c6cdb54629b5f51e345a79e4dd 12 | digest: b5:c745e1521879f43740230b1df673d0729f55704efefdcfc489d4a0a2d40c92a26cacfeab62813403040a8b180142d53b398c7ca784a065e43823605ee49681de 13 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | modules: 3 | - path: proto 4 | deps: 5 | - buf.build/bufbuild/protovalidate:v1.0.0-rc.1 6 | - buf.build/bufbuild/protovalidate-testing:v1.0.0-rc.1 7 | lint: 8 | use: 9 | - STANDARD 10 | ignore_only: 11 | PROTOVALIDATE: 12 | - proto/tests/example/v1/validations.proto 13 | - proto/tests/example/v1/filter.proto 14 | - proto/tests/example/v1/compile.proto 15 | breaking: 16 | use: 17 | - FILE 18 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "sync" 19 | "testing" 20 | 21 | pvcel "buf.build/go/protovalidate/cel" 22 | pb "buf.build/go/protovalidate/internal/gen/tests/example/v1" 23 | "github.com/google/cel-go/cel" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | "google.golang.org/protobuf/proto" 27 | "google.golang.org/protobuf/reflect/protoreflect" 28 | "google.golang.org/protobuf/reflect/protoregistry" 29 | ) 30 | 31 | func TestBuildCache(t *testing.T) { 32 | t.Parallel() 33 | 34 | env, err := cel.NewEnv(cel.Lib(pvcel.NewLibrary())) 35 | require.NoError(t, err, "failed to construct CEL environment") 36 | bldr := newBuilder( 37 | env, false, protoregistry.GlobalTypes, false, 38 | ) 39 | wg := sync.WaitGroup{} 40 | for i := range 100 { 41 | wg.Add(1) 42 | dynamicMsg := dynamicProto{&pb.Person{ 43 | Id: 1234, 44 | Email: "protovalidate@buf.build", 45 | Name: "Protocol Buffer", 46 | }, int32(i)} 47 | desc := dynamicMsg.ProtoReflect().Descriptor() 48 | go func() { 49 | defer wg.Done() 50 | eval := bldr.Load(desc) 51 | assert.NotNil(t, eval) 52 | }() 53 | } 54 | wg.Wait() 55 | } 56 | 57 | type dynamicProto struct { 58 | proto.Message 59 | salt int32 60 | } 61 | 62 | func (d dynamicProto) ProtoReflect() protoreflect.Message { 63 | return dynamicMessage{d.Message.ProtoReflect(), d.salt} 64 | } 65 | 66 | type dynamicMessage struct { 67 | protoreflect.Message 68 | salt int32 69 | } 70 | 71 | func (d dynamicMessage) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) { 72 | d.Message.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { 73 | return f(dynamicFieldDescriptor{fd, d.salt}, v) 74 | }) 75 | } 76 | 77 | func (d dynamicMessage) Has(fd protoreflect.FieldDescriptor) bool { 78 | return d.Message.Has(unwrapFieldDescriptor(fd)) 79 | } 80 | func (d dynamicMessage) Clear(fd protoreflect.FieldDescriptor) { 81 | d.Message.Clear(unwrapFieldDescriptor(fd)) 82 | } 83 | func (d dynamicMessage) Get(fd protoreflect.FieldDescriptor) protoreflect.Value { 84 | return d.Message.Get(unwrapFieldDescriptor(fd)) 85 | } 86 | func (d dynamicMessage) Set(fd protoreflect.FieldDescriptor, v protoreflect.Value) { 87 | d.Message.Set(unwrapFieldDescriptor(fd), v) 88 | } 89 | func (d dynamicMessage) Mutable(fd protoreflect.FieldDescriptor) protoreflect.Value { 90 | return d.Message.Mutable(unwrapFieldDescriptor(fd)) 91 | } 92 | func (d dynamicMessage) NewField(fd protoreflect.FieldDescriptor) protoreflect.Value { 93 | return d.Message.NewField(unwrapFieldDescriptor(fd)) 94 | } 95 | 96 | func (d dynamicMessage) Descriptor() protoreflect.MessageDescriptor { 97 | return dynamicMessageDescriptor{d.Message.Descriptor(), d.salt} 98 | } 99 | 100 | type dynamicMessageDescriptor struct { 101 | protoreflect.MessageDescriptor 102 | salt int32 103 | } 104 | 105 | func (d dynamicMessageDescriptor) Fields() protoreflect.FieldDescriptors { 106 | return dynamicFieldDescriptors{d.MessageDescriptor.Fields(), d.salt} 107 | } 108 | 109 | type dynamicFieldDescriptors struct { 110 | protoreflect.FieldDescriptors 111 | salt int32 112 | } 113 | 114 | func (d dynamicFieldDescriptors) Get(i int) protoreflect.FieldDescriptor { 115 | return dynamicFieldDescriptor{d.FieldDescriptors.Get(i), d.salt} 116 | } 117 | func (d dynamicFieldDescriptors) ByName(s protoreflect.Name) protoreflect.FieldDescriptor { 118 | return dynamicFieldDescriptor{d.FieldDescriptors.ByName(s), d.salt} 119 | } 120 | func (d dynamicFieldDescriptors) ByJSONName(s string) protoreflect.FieldDescriptor { 121 | return dynamicFieldDescriptor{d.FieldDescriptors.ByJSONName(s), d.salt} 122 | } 123 | func (d dynamicFieldDescriptors) ByTextName(s string) protoreflect.FieldDescriptor { 124 | return dynamicFieldDescriptor{d.FieldDescriptors.ByTextName(s), d.salt} 125 | } 126 | func (d dynamicFieldDescriptors) ByNumber(n protoreflect.FieldNumber) protoreflect.FieldDescriptor { 127 | return dynamicFieldDescriptor{d.FieldDescriptors.ByNumber(n), d.salt} 128 | } 129 | 130 | type dynamicFieldDescriptor struct { 131 | protoreflect.FieldDescriptor 132 | salt int32 133 | } 134 | 135 | func unwrapFieldDescriptor(fd protoreflect.FieldDescriptor) protoreflect.FieldDescriptor { 136 | if d, ok := fd.(dynamicFieldDescriptor); ok { 137 | return d.FieldDescriptor 138 | } 139 | return fd 140 | } 141 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | pvcel "buf.build/go/protovalidate/cel" 22 | "buf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases" 23 | "github.com/google/cel-go/cel" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | "google.golang.org/protobuf/proto" 27 | "google.golang.org/protobuf/reflect/protoreflect" 28 | "google.golang.org/protobuf/reflect/protoregistry" 29 | ) 30 | 31 | func getFieldDesc(t *testing.T, msg proto.Message, fld protoreflect.Name) protoreflect.FieldDescriptor { 32 | t.Helper() 33 | desc := msg.ProtoReflect().Descriptor().Fields().ByName(fld) 34 | require.NotNil(t, desc) 35 | return desc 36 | } 37 | 38 | func TestCache_BuildStandardRules(t *testing.T) { 39 | t.Parallel() 40 | 41 | tests := []struct { 42 | name string 43 | desc protoreflect.FieldDescriptor 44 | cons *validate.FieldRules 45 | forItems bool 46 | exCt int 47 | exErr bool 48 | }{ 49 | { 50 | name: "no rules", 51 | desc: getFieldDesc(t, &cases.FloatNone{}, "val"), 52 | cons: &validate.FieldRules{}, 53 | exCt: 0, 54 | }, 55 | { 56 | name: "nil rules", 57 | desc: getFieldDesc(t, &cases.FloatNone{}, "val"), 58 | cons: nil, 59 | exCt: 0, 60 | }, 61 | { 62 | name: "list rules", 63 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 64 | cons: &validate.FieldRules{Type: &validate.FieldRules_Repeated{Repeated: &validate.RepeatedRules{ 65 | MinItems: proto.Uint64(3), 66 | }}}, 67 | exCt: 1, 68 | }, 69 | { 70 | name: "list item rules", 71 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 72 | cons: &validate.FieldRules{Type: &validate.FieldRules_Int64{Int64: &validate.Int64Rules{ 73 | NotIn: []int64{123}, 74 | Const: proto.Int64(456), 75 | }}}, 76 | forItems: true, 77 | exCt: 2, 78 | }, 79 | { 80 | name: "map rules", 81 | desc: getFieldDesc(t, &cases.MapNone{}, "val"), 82 | cons: &validate.FieldRules{Type: &validate.FieldRules_Map{Map: &validate.MapRules{ 83 | MinPairs: proto.Uint64(2), 84 | }}}, 85 | exCt: 1, 86 | }, 87 | { 88 | name: "mismatch rules", 89 | desc: getFieldDesc(t, &cases.AnyNone{}, "val"), 90 | cons: &validate.FieldRules{Type: &validate.FieldRules_Float{Float: &validate.FloatRules{ 91 | Const: proto.Float32(1.23), 92 | }}}, 93 | exErr: true, 94 | }, 95 | } 96 | 97 | env, err := cel.NewEnv(cel.Lib(pvcel.NewLibrary())) 98 | for _, tc := range tests { 99 | test := tc 100 | t.Run(test.name, func(t *testing.T) { 101 | t.Parallel() 102 | require.NoError(t, err) 103 | c := newCache() 104 | 105 | set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, false, test.forItems) 106 | if test.exErr { 107 | assert.Error(t, err) 108 | } else { 109 | require.NoError(t, err) 110 | assert.Len(t, set, test.exCt) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func TestCache_LoadOrCompileStandardRule(t *testing.T) { 117 | t.Parallel() 118 | 119 | env, err := cel.NewEnv(cel.Lib(pvcel.NewLibrary())) 120 | require.NoError(t, err) 121 | 122 | rules := &validate.FieldRules{} 123 | oneOfDesc := rules.ProtoReflect().Descriptor().Oneofs().ByName("type").Fields().ByName("float") 124 | msg := &cases.FloatIn{} 125 | desc := getFieldDesc(t, msg, "val") 126 | require.NotNil(t, desc) 127 | 128 | cache := newCache() 129 | _, ok := cache.cache[desc] 130 | assert.False(t, ok) 131 | 132 | asts, err := cache.loadOrCompileStandardRule(env, oneOfDesc, desc) 133 | require.NoError(t, err) 134 | assert.Nil(t, asts) 135 | 136 | cached, ok := cache.cache[desc] 137 | assert.True(t, ok) 138 | assert.Equal(t, cached, asts) 139 | 140 | asts, err = cache.loadOrCompileStandardRule(env, oneOfDesc, desc) 141 | require.NoError(t, err) 142 | assert.Equal(t, cached, asts) 143 | } 144 | 145 | func TestCache_GetExpectedRuleDescriptor(t *testing.T) { 146 | t.Parallel() 147 | 148 | tests := []struct { 149 | desc protoreflect.FieldDescriptor 150 | forItems bool 151 | ex protoreflect.FieldDescriptor 152 | }{ 153 | { 154 | desc: getFieldDesc(t, &cases.MapNone{}, "val"), 155 | ex: mapFieldRulesDesc, 156 | }, 157 | { 158 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 159 | ex: repeatedFieldRulesDesc, 160 | }, 161 | { 162 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 163 | forItems: true, 164 | ex: expectedStandardRules[protoreflect.Int64Kind], 165 | }, 166 | { 167 | desc: getFieldDesc(t, &cases.AnyNone{}, "val"), 168 | ex: expectedWKTRules["google.protobuf.Any"], 169 | }, 170 | { 171 | desc: getFieldDesc(t, &cases.TimestampNone{}, "val"), 172 | ex: expectedWKTRules["google.protobuf.Timestamp"], 173 | }, 174 | { 175 | desc: getFieldDesc(t, &cases.DurationNone{}, "val"), 176 | ex: expectedWKTRules["google.protobuf.Duration"], 177 | }, 178 | { 179 | desc: getFieldDesc(t, &cases.StringNone{}, "val"), 180 | ex: expectedStandardRules[protoreflect.StringKind], 181 | }, 182 | { 183 | desc: getFieldDesc(t, &cases.MessageNone{}, "val"), 184 | ex: nil, 185 | }, 186 | } 187 | 188 | c := newCache() 189 | for _, tc := range tests { 190 | test := tc 191 | t.Run(string(test.desc.FullName()), func(t *testing.T) { 192 | t.Parallel() 193 | out, ok := c.getExpectedRuleDescriptor(test.desc, test.forItems) 194 | if test.ex != nil { 195 | assert.True(t, ok) 196 | assert.Equal(t, test.ex.FullName(), out.FullName()) 197 | } else { 198 | assert.False(t, ok) 199 | } 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /cel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "errors" 19 | 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | // celPrograms is an evaluator that executes an expression.ProgramSet. 24 | type celPrograms struct { 25 | base 26 | programSet 27 | } 28 | 29 | func (c celPrograms) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 30 | err := c.Eval(val, cfg) 31 | if err != nil { 32 | var valErr *ValidationError 33 | if errors.As(err, &valErr) { 34 | for _, violation := range valErr.Violations { 35 | violation.Proto.Field = c.fieldPath() 36 | violation.Proto.Rule = c.rulePath(violation.Proto.GetRule()) 37 | violation.FieldValue = val 38 | violation.FieldDescriptor = c.Descriptor 39 | } 40 | } 41 | } 42 | return err 43 | } 44 | 45 | func (c celPrograms) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { 46 | return c.Eval(protoreflect.ValueOfMessage(msg), cfg) 47 | } 48 | 49 | func (c celPrograms) Tautology() bool { 50 | return len(c.programSet) == 0 51 | } 52 | 53 | var ( 54 | _ evaluator = celPrograms{} 55 | _ messageEvaluator = celPrograms{} 56 | ) 57 | -------------------------------------------------------------------------------- /cel/library_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 cel 16 | 17 | import ( 18 | "testing" 19 | 20 | "buf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases" 21 | "github.com/google/cel-go/cel" 22 | "github.com/google/cel-go/common/types" 23 | "github.com/google/cel-go/common/types/ref" 24 | "github.com/google/cel-go/interpreter" 25 | "github.com/stretchr/testify/assert" 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | func TestCELLib(t *testing.T) { 30 | t.Parallel() 31 | 32 | testValue := cases.StringConst_builder{Val: "test_string"}.Build() 33 | 34 | activation, err := interpreter.NewActivation(map[string]any{ 35 | "test": testValue, 36 | }) 37 | require.NoError(t, err) 38 | 39 | env, err := cel.NewEnv( 40 | cel.Lib(NewLibrary()), 41 | cel.Variable( 42 | "test", 43 | cel.ObjectType( 44 | string(testValue.ProtoReflect().Descriptor().FullName()), 45 | ), 46 | ), 47 | ) 48 | 49 | require.NoError(t, err) 50 | 51 | t.Run("ext", func(t *testing.T) { 52 | t.Parallel() 53 | 54 | tests := []struct { 55 | expr string 56 | ex any 57 | }{ 58 | {"0.0.isInf()", false}, 59 | {"0.0.isNan()", false}, 60 | {"(1.0/0.0).isInf()", true}, 61 | {"(1.0/0.0).isInf(0)", true}, 62 | {"(1.0/0.0).isInf(1)", true}, 63 | {"(1.0/0.0).isInf(-1)", false}, 64 | {"(-1.0/0.0).isInf()", true}, 65 | {"(-1.0/0.0).isInf(0)", true}, 66 | {"(-1.0/0.0).isInf(1)", false}, 67 | {"(-1.0/0.0).isInf(-1)", true}, 68 | {"(0.0/0.0).isNan()", true}, 69 | {"(0.0/0.0).isInf()", false}, 70 | {"(1.0/0.0).isNan()", false}, 71 | { 72 | "[].unique()", 73 | true, 74 | }, 75 | { 76 | "[true].unique()", 77 | true, 78 | }, 79 | { 80 | "[true, false].unique()", 81 | true, 82 | }, 83 | { 84 | "[true, true].unique()", 85 | false, 86 | }, 87 | { 88 | "[1, 2, 3].unique()", 89 | true, 90 | }, 91 | { 92 | "[1, 2, 1].unique()", 93 | false, 94 | }, 95 | { 96 | "[1u, 2u, 3u].unique()", 97 | true, 98 | }, 99 | { 100 | "[1u, 2u, 2u].unique()", 101 | false, 102 | }, 103 | { 104 | "[1.0, 2.0, 3.0].unique()", 105 | true, 106 | }, 107 | { 108 | "[3.0,2.0,3.0].unique()", 109 | false, 110 | }, 111 | { 112 | "['abc', 'def'].unique()", 113 | true, 114 | }, 115 | { 116 | "['abc', 'abc'].unique()", 117 | false, 118 | }, 119 | { 120 | "[b'abc', b'123'].unique()", 121 | true, 122 | }, 123 | { 124 | "[b'123', b'123'].unique()", 125 | false, 126 | }, 127 | { 128 | "'1.2.3.0/24'.isIpPrefix()", 129 | true, 130 | }, 131 | { 132 | "'1.2.3.4/24'.isIpPrefix()", 133 | true, 134 | }, 135 | { 136 | "'1.2.3.0/24'.isIpPrefix(true)", 137 | true, 138 | }, 139 | { 140 | "'1.2.3.4/24'.isIpPrefix(true)", 141 | false, 142 | }, 143 | { 144 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix()", 145 | true, 146 | }, 147 | { 148 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix()", 149 | true, 150 | }, 151 | { 152 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(true)", 153 | false, 154 | }, 155 | { 156 | "'1.2.3.4'.isIpPrefix()", 157 | false, 158 | }, 159 | { 160 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b'.isIpPrefix()", 161 | false, 162 | }, 163 | { 164 | "'1.2.3.0/24'.isIpPrefix(4)", 165 | true, 166 | }, 167 | { 168 | "'1.2.3.4/24'.isIpPrefix(4)", 169 | true, 170 | }, 171 | { 172 | "'1.2.3.0/24'.isIpPrefix(4,true)", 173 | true, 174 | }, 175 | { 176 | "'1.2.3.4/24'.isIpPrefix(4,true)", 177 | false, 178 | }, 179 | { 180 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(4)", 181 | false, 182 | }, 183 | { 184 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6)", 185 | true, 186 | }, 187 | { 188 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6)", 189 | true, 190 | }, 191 | { 192 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:4000/118'.isIpPrefix(6,true)", 193 | true, 194 | }, 195 | { 196 | "'fd7a:115c:a1e0:ab12:4843:cd96:626b:430b/118'.isIpPrefix(6,true)", 197 | false, 198 | }, 199 | { 200 | "'1.2.3.0/24'.isIpPrefix(6)", 201 | false, 202 | }, 203 | { 204 | "'foo@example.com'.isEmail()", 205 | true, 206 | }, 207 | { 208 | "''.isEmail()", 209 | false, 210 | }, 211 | { 212 | "' foo@example.com'.isEmail()", 213 | false, 214 | }, 215 | { 216 | "'foo@example.com '.isEmail()", 217 | false, 218 | }, 219 | { 220 | "getField(test, 'val')", 221 | "test_string", 222 | }, 223 | { 224 | "getField(test, 'lav')", 225 | types.NewErrFromString("no such field"), 226 | }, 227 | { 228 | "getField(0, 'val')", 229 | types.NewErrFromString("unsupported conversion"), 230 | }, 231 | } 232 | 233 | for _, tc := range tests { 234 | test := tc 235 | t.Run(test.expr, func(t *testing.T) { 236 | t.Parallel() 237 | prog := buildTestProgram(t, env, test.expr) 238 | val, _, err := prog.Eval(activation) 239 | if refEx, ok := test.ex.(ref.Val); ok && types.IsError(refEx) { 240 | refErr, ok := refEx.Value().(error) 241 | require.True(t, ok) 242 | assert.ErrorContains(t, err, refErr.Error()) 243 | } else { 244 | require.NoError(t, err) 245 | assert.Equal(t, test.ex, val.Value()) 246 | } 247 | }) 248 | } 249 | }) 250 | } 251 | 252 | func buildTestProgram(t *testing.T, env *cel.Env, expr string) cel.Program { 253 | t.Helper() 254 | ast, issues := env.Compile(expr) 255 | require.NoError(t, issues.Err()) 256 | prog, err := env.Program(ast) 257 | require.NoError(t, err) 258 | return prog 259 | } 260 | 261 | func TestIsUri(t *testing.T) { 262 | t.Parallel() 263 | require.True(t, isURI("A://")) 264 | } 265 | 266 | func TestIsHostname(t *testing.T) { 267 | t.Parallel() 268 | require.True(t, isHostname("foo.example.com")) 269 | require.True(t, isHostname("A.ISI.EDU")) 270 | require.False(t, isHostname("İ")) 271 | } 272 | 273 | func TestIsHostAndPort(t *testing.T) { 274 | t.Parallel() 275 | require.False(t, isHostAndPort("example.com:080", false)) 276 | require.False(t, isHostAndPort("example.com:00", false)) 277 | } 278 | -------------------------------------------------------------------------------- /cel/lookups.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 cel 16 | 17 | import ( 18 | "github.com/google/cel-go/cel" 19 | "github.com/google/cel-go/common/types" 20 | "github.com/google/cel-go/common/types/ref" 21 | "google.golang.org/protobuf/reflect/protoreflect" 22 | ) 23 | 24 | // ProtoFieldToType resolves the CEL value type for the provided 25 | // FieldDescriptor. If generic is true, the specific subtypes of map and 26 | // repeated fields will be replaced with cel.DynType. If forItems is true, the 27 | // type for the repeated list items is returned instead of the list type itself. 28 | func ProtoFieldToType(fieldDesc protoreflect.FieldDescriptor, generic, forItems bool) *cel.Type { 29 | if !forItems { 30 | switch { 31 | case fieldDesc.IsMap(): 32 | if generic { 33 | return cel.MapType(cel.DynType, cel.DynType) 34 | } 35 | keyType := ProtoFieldToType(fieldDesc.MapKey(), false, true) 36 | valType := ProtoFieldToType(fieldDesc.MapValue(), false, true) 37 | return cel.MapType(keyType, valType) 38 | case fieldDesc.IsList(): 39 | if generic { 40 | return cel.ListType(cel.DynType) 41 | } 42 | itemType := ProtoFieldToType(fieldDesc, false, true) 43 | return cel.ListType(itemType) 44 | } 45 | } 46 | 47 | if fieldDesc.Kind() == protoreflect.MessageKind || 48 | fieldDesc.Kind() == protoreflect.GroupKind { 49 | switch fqn := fieldDesc.Message().FullName(); fqn { 50 | case "google.protobuf.Any": 51 | return cel.AnyType 52 | case "google.protobuf.Duration": 53 | return cel.DurationType 54 | case "google.protobuf.Timestamp": 55 | return cel.TimestampType 56 | default: 57 | return cel.ObjectType(string(fqn)) 58 | } 59 | } 60 | return protoKindToType(fieldDesc.Kind()) 61 | } 62 | 63 | func ProtoFieldToValue(fieldDesc protoreflect.FieldDescriptor, value protoreflect.Value, forItems bool) ref.Val { 64 | switch { 65 | case fieldDesc.IsList() && !forItems: 66 | return types.NewProtoList(types.DefaultTypeAdapter, value.List()) 67 | default: 68 | return types.DefaultTypeAdapter.NativeToValue(value.Interface()) 69 | } 70 | } 71 | 72 | // protoKindToType maps a protoreflect.Kind to a compatible cel.Type. 73 | func protoKindToType(kind protoreflect.Kind) *cel.Type { 74 | switch kind { 75 | case 76 | protoreflect.FloatKind, 77 | protoreflect.DoubleKind: 78 | return cel.DoubleType 79 | case 80 | protoreflect.Int32Kind, 81 | protoreflect.Int64Kind, 82 | protoreflect.Sint32Kind, 83 | protoreflect.Sint64Kind, 84 | protoreflect.Sfixed32Kind, 85 | protoreflect.Sfixed64Kind, 86 | protoreflect.EnumKind: 87 | return cel.IntType 88 | case 89 | protoreflect.Uint32Kind, 90 | protoreflect.Uint64Kind, 91 | protoreflect.Fixed32Kind, 92 | protoreflect.Fixed64Kind: 93 | return cel.UintType 94 | case protoreflect.BoolKind: 95 | return cel.BoolType 96 | case protoreflect.StringKind: 97 | return cel.StringType 98 | case protoreflect.BytesKind: 99 | return cel.BytesType 100 | case 101 | protoreflect.MessageKind, 102 | protoreflect.GroupKind: 103 | return cel.DynType 104 | default: 105 | return cel.DynType 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cel/lookups_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 cel 16 | 17 | import ( 18 | "testing" 19 | 20 | "buf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases" 21 | "github.com/google/cel-go/cel" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | "google.golang.org/protobuf/proto" 25 | "google.golang.org/protobuf/reflect/protoreflect" 26 | ) 27 | 28 | func TestCache_GetCELType(t *testing.T) { 29 | t.Parallel() 30 | 31 | tests := []struct { 32 | desc protoreflect.FieldDescriptor 33 | generic bool 34 | forItems bool 35 | ex *cel.Type 36 | }{ 37 | { 38 | desc: getFieldDesc(t, &cases.MapNone{}, "val"), 39 | ex: cel.MapType(cel.UintType, cel.BoolType), 40 | }, 41 | { 42 | desc: getFieldDesc(t, &cases.MapNone{}, "val"), 43 | generic: true, 44 | ex: cel.MapType(cel.DynType, cel.DynType), 45 | }, 46 | { 47 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 48 | ex: cel.ListType(cel.IntType), 49 | }, 50 | { 51 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 52 | generic: true, 53 | ex: cel.ListType(cel.DynType), 54 | }, 55 | { 56 | desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), 57 | forItems: true, 58 | ex: cel.IntType, 59 | }, 60 | { 61 | desc: getFieldDesc(t, &cases.AnyNone{}, "val"), 62 | ex: cel.AnyType, 63 | }, 64 | { 65 | desc: getFieldDesc(t, &cases.DurationNone{}, "val"), 66 | ex: cel.DurationType, 67 | }, 68 | { 69 | desc: getFieldDesc(t, &cases.TimestampNone{}, "val"), 70 | ex: cel.TimestampType, 71 | }, 72 | { 73 | desc: getFieldDesc(t, &cases.MessageNone{}, "val"), 74 | ex: cel.ObjectType(string(((&cases.MessageNone{}).GetVal()).ProtoReflect().Descriptor().FullName())), 75 | }, 76 | { 77 | desc: getFieldDesc(t, &cases.Int32None{}, "val"), 78 | ex: cel.IntType, 79 | }, 80 | } 81 | 82 | for _, tc := range tests { 83 | test := tc 84 | t.Run(string(test.desc.FullName()), func(t *testing.T) { 85 | t.Parallel() 86 | typ := ProtoFieldToType(test.desc, test.generic, test.forItems) 87 | assert.Equal(t, test.ex.String(), typ.String()) 88 | }) 89 | } 90 | } 91 | 92 | func TestProtoKindToCELType(t *testing.T) { 93 | t.Parallel() 94 | 95 | tests := map[protoreflect.Kind]*cel.Type{ 96 | protoreflect.FloatKind: cel.DoubleType, 97 | protoreflect.DoubleKind: cel.DoubleType, 98 | protoreflect.Int32Kind: cel.IntType, 99 | protoreflect.Int64Kind: cel.IntType, 100 | protoreflect.Uint32Kind: cel.UintType, 101 | protoreflect.Uint64Kind: cel.UintType, 102 | protoreflect.Sint32Kind: cel.IntType, 103 | protoreflect.Sint64Kind: cel.IntType, 104 | protoreflect.Fixed32Kind: cel.UintType, 105 | protoreflect.Fixed64Kind: cel.UintType, 106 | protoreflect.Sfixed32Kind: cel.IntType, 107 | protoreflect.Sfixed64Kind: cel.IntType, 108 | protoreflect.BoolKind: cel.BoolType, 109 | protoreflect.StringKind: cel.StringType, 110 | protoreflect.BytesKind: cel.BytesType, 111 | protoreflect.EnumKind: cel.IntType, 112 | protoreflect.MessageKind: cel.DynType, 113 | protoreflect.GroupKind: cel.DynType, 114 | protoreflect.Kind(0): cel.DynType, 115 | } 116 | 117 | for k, ty := range tests { 118 | kind, typ := k, ty 119 | t.Run(kind.String(), func(t *testing.T) { 120 | t.Parallel() 121 | assert.Equal(t, typ, protoKindToType(kind)) 122 | }) 123 | } 124 | } 125 | 126 | func getFieldDesc(t *testing.T, msg proto.Message, fld protoreflect.Name) protoreflect.FieldDescriptor { 127 | t.Helper() 128 | desc := msg.ProtoReflect().Descriptor().Fields().ByName(fld) 129 | require.NotNil(t, desc) 130 | return desc 131 | } 132 | -------------------------------------------------------------------------------- /compilation_error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import "strings" 18 | 19 | // A CompilationError is returned if a CEL expression cannot be compiled & 20 | // type-checked or if invalid standard rules are applied. 21 | type CompilationError struct { 22 | cause error 23 | } 24 | 25 | func (err *CompilationError) Error() string { 26 | if err == nil { 27 | return "" 28 | } 29 | var builder strings.Builder 30 | _, _ = builder.WriteString("compilation error") 31 | if err.cause != nil { 32 | _, _ = builder.WriteString(": ") 33 | _, _ = builder.WriteString(err.cause.Error()) 34 | } 35 | return builder.String() 36 | } 37 | 38 | func (err *CompilationError) Unwrap() error { 39 | if err == nil { 40 | return nil 41 | } 42 | return err.cause 43 | } 44 | -------------------------------------------------------------------------------- /compile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | "github.com/google/cel-go/cel" 22 | ) 23 | 24 | // An expressions instance is a container for the information needed to compile 25 | // and evaluate a list of CEL-based expressions, originating from a 26 | // validate.Rule. 27 | type expressions struct { 28 | Rules []*validate.Rule 29 | RulePath []*validate.FieldPathElement 30 | } 31 | 32 | // compile produces a ProgramSet from the provided expressions in the given 33 | // environment. If the generated cel.Program require cel.ProgramOption params, 34 | // use CompileASTs instead with a subsequent call to ASTSet.ToProgramSet. 35 | func compile( 36 | expressions expressions, 37 | env *cel.Env, 38 | envOpts ...cel.EnvOption, 39 | ) (set programSet, err error) { 40 | if len(expressions.Rules) == 0 { 41 | return nil, nil 42 | } 43 | 44 | if len(envOpts) > 0 { 45 | env, err = env.Extend(envOpts...) 46 | if err != nil { 47 | return nil, &CompilationError{cause: fmt.Errorf( 48 | "failed to extend environment: %w", err)} 49 | } 50 | } 51 | 52 | set = make(programSet, len(expressions.Rules)) 53 | for i, rule := range expressions.Rules { 54 | set[i].Source = rule 55 | set[i].Path = expressions.RulePath 56 | 57 | ast, err := compileAST(env, rule, expressions.RulePath) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | set[i], err = ast.toProgram(env) 63 | if err != nil { 64 | return nil, err 65 | } 66 | } 67 | return set, nil 68 | } 69 | 70 | // compileASTs parses and type checks a set of expressions, producing a resulting 71 | // ASTSet. The value can then be converted to a ProgramSet via 72 | // ASTSet.ToProgramSet or ASTSet.ReduceResiduals. Use Compile instead if no 73 | // cel.ProgramOption args need to be provided or residuals do not need to be 74 | // computed. 75 | func compileASTs( 76 | expressions expressions, 77 | env *cel.Env, 78 | envOpts ...cel.EnvOption, 79 | ) (set astSet, err error) { 80 | if len(expressions.Rules) == 0 { 81 | return set, nil 82 | } 83 | 84 | set = make([]compiledAST, len(expressions.Rules)) 85 | for i, rule := range expressions.Rules { 86 | set[i].Env = env 87 | if len(envOpts) > 0 { 88 | set[i].Env, err = env.Extend(envOpts...) 89 | if err != nil { 90 | return set, &CompilationError{cause: fmt.Errorf( 91 | "failed to extend environment: %w", err)} 92 | } 93 | } 94 | set[i], err = compileAST(set[i].Env, rule, expressions.RulePath) 95 | if err != nil { 96 | return set, err 97 | } 98 | } 99 | 100 | return set, nil 101 | } 102 | 103 | func compileAST(env *cel.Env, rule *validate.Rule, rulePath []*validate.FieldPathElement) (out compiledAST, err error) { 104 | ast, issues := env.Compile(rule.GetExpression()) 105 | if err := issues.Err(); err != nil { 106 | return out, &CompilationError{cause: fmt.Errorf( 107 | "failed to compile expression %s: %w", rule.GetId(), err)} 108 | } 109 | 110 | outType := ast.OutputType() 111 | if !(outType.IsAssignableType(cel.BoolType) || outType.IsAssignableType(cel.StringType)) { 112 | return out, &CompilationError{cause: fmt.Errorf( 113 | "expression %s outputs %s, wanted either bool or string", 114 | rule.GetId(), outType.String())} 115 | } 116 | 117 | return compiledAST{ 118 | AST: ast, 119 | Env: env, 120 | Source: rule, 121 | Path: rulePath, 122 | }, nil 123 | } 124 | -------------------------------------------------------------------------------- /compile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | "github.com/google/cel-go/cel" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | "google.golang.org/protobuf/proto" 25 | ) 26 | 27 | func TestCompile(t *testing.T) { 28 | t.Parallel() 29 | 30 | baseEnv, err := cel.NewEnv() 31 | baseEnv.Compile("true") 32 | require.NoError(t, err) 33 | 34 | t.Run("empty", func(t *testing.T) { 35 | t.Parallel() 36 | var exprs expressions 37 | set, err := compile(exprs, baseEnv) 38 | assert.Nil(t, set) 39 | require.NoError(t, err) 40 | }) 41 | 42 | t.Run("success", func(t *testing.T) { 43 | t.Parallel() 44 | exprs := expressions{ 45 | Rules: []*validate.Rule{ 46 | {Id: proto.String("foo"), Expression: proto.String("this == 123")}, 47 | {Id: proto.String("bar"), Expression: proto.String("'a string'")}, 48 | }, 49 | } 50 | set, err := compile(exprs, baseEnv, cel.Variable("this", cel.IntType)) 51 | assert.Len(t, set, len(exprs.Rules)) 52 | require.NoError(t, err) 53 | }) 54 | 55 | t.Run("env extension err", func(t *testing.T) { 56 | t.Parallel() 57 | exprs := expressions{ 58 | Rules: []*validate.Rule{ 59 | {Id: proto.String("foo"), Expression: proto.String("0 != 0")}, 60 | }, 61 | } 62 | set, err := compile(exprs, baseEnv, cel.Types(true)) 63 | assert.Nil(t, set) 64 | var compErr *CompilationError 65 | require.ErrorAs(t, err, &compErr) 66 | }) 67 | 68 | t.Run("bad syntax", func(t *testing.T) { 69 | t.Parallel() 70 | exprs := expressions{ 71 | Rules: []*validate.Rule{ 72 | {Id: proto.String("foo"), Expression: proto.String("!@#$%^&")}, 73 | }, 74 | } 75 | set, err := compile(exprs, baseEnv) 76 | assert.Nil(t, set) 77 | var compErr *CompilationError 78 | require.ErrorAs(t, err, &compErr) 79 | }) 80 | 81 | t.Run("invalid output type", func(t *testing.T) { 82 | t.Parallel() 83 | exprs := expressions{ 84 | Rules: []*validate.Rule{ 85 | {Id: proto.String("foo"), Expression: proto.String("1.23")}, 86 | }, 87 | } 88 | set, err := compile(exprs, baseEnv) 89 | assert.Nil(t, set) 90 | var compErr *CompilationError 91 | require.ErrorAs(t, err, &compErr) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /conformance/expected_failures.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bufbuild/protovalidate-go/1a8547ed7493328236b4c9d25f6c9f7cf0252604/conformance/expected_failures.yaml -------------------------------------------------------------------------------- /enum.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | //nolint:gochecknoglobals 24 | var ( 25 | enumRuleDescriptor = (&validate.FieldRules{}).ProtoReflect().Descriptor().Fields().ByName("enum") 26 | enumDefinedOnlyRuleDescriptor = (&validate.EnumRules{}).ProtoReflect().Descriptor().Fields().ByName("defined_only") 27 | enumDefinedOnlyRulePath = &validate.FieldPath{ 28 | Elements: []*validate.FieldPathElement{ 29 | fieldPathElement(enumRuleDescriptor), 30 | fieldPathElement(enumDefinedOnlyRuleDescriptor), 31 | }, 32 | } 33 | ) 34 | 35 | // definedEnum is an evaluator that checks an enum value being a member of 36 | // the defined values exclusively. This check is handled outside CEL as enums 37 | // are completely type erased to integers. 38 | type definedEnum struct { 39 | base 40 | 41 | // ValueDescriptors captures all the defined values for this enum 42 | ValueDescriptors protoreflect.EnumValueDescriptors 43 | } 44 | 45 | func (d definedEnum) Evaluate(_ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error { 46 | if d.ValueDescriptors.ByNumber(val.Enum()) == nil { 47 | return &ValidationError{Violations: []*Violation{{ 48 | Proto: &validate.Violation{ 49 | Field: d.fieldPath(), 50 | Rule: d.rulePath(enumDefinedOnlyRulePath), 51 | RuleId: proto.String("enum.defined_only"), 52 | Message: proto.String("value must be one of the defined enum values"), 53 | }, 54 | FieldValue: val, 55 | FieldDescriptor: d.Descriptor, 56 | RuleValue: protoreflect.ValueOfBool(true), 57 | RuleDescriptor: enumDefinedOnlyRuleDescriptor, 58 | }}} 59 | } 60 | return nil 61 | } 62 | 63 | func (d definedEnum) Tautology() bool { 64 | return false 65 | } 66 | 67 | var _ evaluator = definedEnum{} 68 | -------------------------------------------------------------------------------- /error_utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "errors" 19 | "slices" 20 | "strconv" 21 | "strings" 22 | 23 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 24 | "google.golang.org/protobuf/proto" 25 | "google.golang.org/protobuf/reflect/protoreflect" 26 | "google.golang.org/protobuf/types/descriptorpb" 27 | ) 28 | 29 | // mergeViolations is a utility to resolve and combine errors resulting from 30 | // evaluation. If ok is false, execution of validation should stop (either due 31 | // to failFast or the result is not a ValidationError). 32 | // 33 | //nolint:errorlint 34 | func mergeViolations(dst, src error, cfg *validationConfig) (ok bool, err error) { 35 | if src == nil { 36 | return true, dst 37 | } 38 | 39 | srcValErrs, ok := src.(*ValidationError) 40 | if !ok { 41 | return false, src 42 | } 43 | 44 | if dst == nil { 45 | return !(cfg.failFast && len(srcValErrs.Violations) > 0), src 46 | } 47 | 48 | dstValErrs, ok := dst.(*ValidationError) 49 | if !ok { 50 | // what should we do here? 51 | return false, dst 52 | } 53 | 54 | dstValErrs.Violations = append(dstValErrs.Violations, srcValErrs.Violations...) 55 | return !(cfg.failFast && len(dstValErrs.Violations) > 0), dst 56 | } 57 | 58 | // fieldPathElement returns a buf.validate.fieldPathElement that corresponds to 59 | // a provided FieldDescriptor. If the provided FieldDescriptor is nil, nil is 60 | // returned. 61 | func fieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathElement { 62 | if field == nil { 63 | return nil 64 | } 65 | return &validate.FieldPathElement{ 66 | FieldNumber: proto.Int32(int32(field.Number())), 67 | FieldName: proto.String(field.TextName()), 68 | FieldType: descriptorpb.FieldDescriptorProto_Type(field.Kind()).Enum(), 69 | } 70 | } 71 | 72 | // fieldPath returns a single-element buf.validate.fieldPath corresponding to 73 | // the provided FieldDescriptor, or nil if the provided FieldDescriptor is nil. 74 | func fieldPath(field protoreflect.FieldDescriptor) *validate.FieldPath { 75 | if field == nil { 76 | return nil 77 | } 78 | return &validate.FieldPath{ 79 | Elements: []*validate.FieldPathElement{ 80 | fieldPathElement(field), 81 | }, 82 | } 83 | } 84 | 85 | // updateViolationPaths modifies the field and rule paths of an error, appending 86 | // an element to the end of each field path (if provided) and prepending a list 87 | // of elements to the beginning of each rule path (if provided.) 88 | // 89 | // Note that this function is ordinarily used to append field paths in reverse 90 | // order, as the stack bubbles up through the evaluators. Then, at the end, the 91 | // path is reversed. Rule paths are generally static, so this optimization isn't 92 | // applied for rule paths. 93 | func updateViolationPaths(err error, fieldSuffix *validate.FieldPathElement, rulePrefix []*validate.FieldPathElement) { 94 | if fieldSuffix == nil && len(rulePrefix) == 0 { 95 | return 96 | } 97 | var valErr *ValidationError 98 | if errors.As(err, &valErr) { 99 | for _, violation := range valErr.Violations { 100 | if fieldSuffix != nil { 101 | if violation.Proto.GetField() == nil { 102 | violation.Proto.Field = &validate.FieldPath{} 103 | } 104 | violation.Proto.Field.Elements = append(violation.Proto.Field.Elements, fieldSuffix) 105 | } 106 | if len(rulePrefix) != 0 { 107 | violation.Proto.Rule.Elements = slices.Concat(rulePrefix, violation.Proto.GetRule().GetElements()) 108 | } 109 | } 110 | } 111 | } 112 | 113 | // finalizeViolationPaths reverses all field paths in the error and populates 114 | // the deprecated string-based field path. 115 | func finalizeViolationPaths(err error) { 116 | var valErr *ValidationError 117 | if errors.As(err, &valErr) { 118 | for _, violation := range valErr.Violations { 119 | if violation.Proto.GetField() != nil { 120 | slices.Reverse(violation.Proto.GetField().GetElements()) 121 | } 122 | } 123 | } 124 | } 125 | 126 | // FieldPathString takes a FieldPath and encodes it to a string-based dotted 127 | // field path. 128 | func FieldPathString(path *validate.FieldPath) string { 129 | var result strings.Builder 130 | for i, element := range path.GetElements() { 131 | if i > 0 { 132 | result.WriteByte('.') 133 | } 134 | result.WriteString(element.GetFieldName()) 135 | subscript := element.GetSubscript() 136 | if subscript == nil { 137 | continue 138 | } 139 | result.WriteByte('[') 140 | switch value := subscript.(type) { 141 | case *validate.FieldPathElement_Index: 142 | result.WriteString(strconv.FormatUint(value.Index, 10)) 143 | case *validate.FieldPathElement_BoolKey: 144 | result.WriteString(strconv.FormatBool(value.BoolKey)) 145 | case *validate.FieldPathElement_IntKey: 146 | result.WriteString(strconv.FormatInt(value.IntKey, 10)) 147 | case *validate.FieldPathElement_UintKey: 148 | result.WriteString(strconv.FormatUint(value.UintKey, 10)) 149 | case *validate.FieldPathElement_StringKey: 150 | result.WriteString(strconv.Quote(value.StringKey)) 151 | } 152 | result.WriteByte(']') 153 | } 154 | return result.String() 155 | } 156 | 157 | // markViolationForKey marks the provided error as being for a map key, by 158 | // setting the `for_key` flag on each violation within the validation error. 159 | func markViolationForKey(err error) { 160 | var valErr *ValidationError 161 | if errors.As(err, &valErr) { 162 | for _, violation := range valErr.Violations { 163 | violation.Proto.ForKey = proto.Bool(true) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /error_utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | 21 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | "google.golang.org/protobuf/proto" 25 | ) 26 | 27 | func TestMerge(t *testing.T) { 28 | t.Parallel() 29 | 30 | t.Run("no errors", func(t *testing.T) { 31 | t.Parallel() 32 | ok, err := mergeViolations(nil, nil, &validationConfig{failFast: true}) 33 | require.NoError(t, err) 34 | assert.True(t, ok) 35 | ok, err = mergeViolations(nil, nil, &validationConfig{failFast: false}) 36 | require.NoError(t, err) 37 | assert.True(t, ok) 38 | }) 39 | 40 | t.Run("no dst", func(t *testing.T) { 41 | t.Parallel() 42 | 43 | t.Run("non-validation", func(t *testing.T) { 44 | t.Parallel() 45 | someErr := errors.New("some error") 46 | ok, err := mergeViolations(nil, someErr, &validationConfig{failFast: true}) 47 | assert.Equal(t, someErr, err) 48 | assert.False(t, ok) 49 | ok, err = mergeViolations(nil, someErr, &validationConfig{failFast: false}) 50 | assert.Equal(t, someErr, err) 51 | assert.False(t, ok) 52 | }) 53 | 54 | t.Run("validation", func(t *testing.T) { 55 | t.Parallel() 56 | exErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{RuleId: proto.String("foo")}}}} 57 | ok, err := mergeViolations(nil, exErr, &validationConfig{failFast: true}) 58 | var valErr *ValidationError 59 | require.ErrorAs(t, err, &valErr) 60 | assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) 61 | assert.False(t, ok) 62 | ok, err = mergeViolations(nil, exErr, &validationConfig{failFast: false}) 63 | require.ErrorAs(t, err, &valErr) 64 | assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) 65 | assert.True(t, ok) 66 | }) 67 | }) 68 | 69 | t.Run("merge", func(t *testing.T) { 70 | t.Parallel() 71 | 72 | t.Run("non-validation dst", func(t *testing.T) { 73 | t.Parallel() 74 | dstErr := errors.New("some error") 75 | srcErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{RuleId: proto.String("foo")}}}} 76 | ok, err := mergeViolations(dstErr, srcErr, &validationConfig{failFast: true}) 77 | assert.Equal(t, dstErr, err) 78 | assert.False(t, ok) 79 | ok, err = mergeViolations(dstErr, srcErr, &validationConfig{failFast: false}) 80 | assert.Equal(t, dstErr, err) 81 | assert.False(t, ok) 82 | }) 83 | 84 | t.Run("non-validation src", func(t *testing.T) { 85 | t.Parallel() 86 | dstErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{RuleId: proto.String("foo")}}}} 87 | srcErr := errors.New("some error") 88 | ok, err := mergeViolations(dstErr, srcErr, &validationConfig{failFast: true}) 89 | assert.Equal(t, srcErr, err) 90 | assert.False(t, ok) 91 | ok, err = mergeViolations(dstErr, srcErr, &validationConfig{failFast: false}) 92 | assert.Equal(t, srcErr, err) 93 | assert.False(t, ok) 94 | }) 95 | 96 | t.Run("validation", func(t *testing.T) { 97 | t.Parallel() 98 | 99 | dstErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{RuleId: proto.String("foo")}}}} 100 | srcErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{RuleId: proto.String("bar")}}}} 101 | exErr := &ValidationError{Violations: []*Violation{ 102 | {Proto: &validate.Violation{RuleId: proto.String("foo")}}, 103 | {Proto: &validate.Violation{RuleId: proto.String("bar")}}, 104 | }} 105 | ok, err := mergeViolations(dstErr, srcErr, &validationConfig{failFast: true}) 106 | var valErr *ValidationError 107 | require.ErrorAs(t, err, &valErr) 108 | assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) 109 | assert.False(t, ok) 110 | dstErr = &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{RuleId: proto.String("foo")}}}} 111 | ok, err = mergeViolations(dstErr, srcErr, &validationConfig{failFast: false}) 112 | require.ErrorAs(t, err, &valErr) 113 | assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) 114 | assert.True(t, ok) 115 | }) 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /evaluator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "google.golang.org/protobuf/reflect/protoreflect" 19 | ) 20 | 21 | // evaluator defines a validation evaluator. evaluator implementations may elide 22 | // type checking of the passed in value, as the types have been guaranteed 23 | // during the build phase. 24 | type evaluator interface { 25 | // Tautology returns true if the evaluator always succeeds. 26 | Tautology() bool 27 | 28 | // Evaluate checks that the provided val is valid. Unless failFast is true, 29 | // evaluation attempts to find all violations present in val instead of 30 | // returning an error on the first violation. The returned error will be one 31 | // of the following expected types: 32 | // 33 | // - errors.ValidationError: val is invalid. 34 | // - errors.RuntimeError: error evaluating val determined at runtime. 35 | // - errors.CompilationError: this evaluator (or child evaluator) failed to 36 | // build. This error is not recoverable. 37 | // 38 | Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error 39 | } 40 | 41 | // messageEvaluator is essentially the same as evaluator, but specialized for 42 | // messages as an optimization. See evaluator for behavior. 43 | type messageEvaluator interface { 44 | evaluator 45 | 46 | // EvaluateMessage checks that the provided msg is valid. See 47 | // evaluator.Evaluate for behavior 48 | EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error 49 | } 50 | 51 | // evaluators are a set of evaluator applied together to a value. Evaluation 52 | // merges all errors.ValidationError violations or short-circuits if failFast is 53 | // true or a different error is returned. 54 | type evaluators []evaluator 55 | 56 | func (e evaluators) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) (err error) { 57 | var ok bool 58 | for _, eval := range e { 59 | evalErr := eval.Evaluate(msg, val, cfg) 60 | if ok, err = mergeViolations(err, evalErr, cfg); !ok { 61 | return err 62 | } 63 | } 64 | return err 65 | } 66 | 67 | func (e evaluators) Tautology() bool { 68 | for _, eval := range e { 69 | if !eval.Tautology() { 70 | return false 71 | } 72 | } 73 | return true 74 | } 75 | 76 | // messageEvaluators are a specialization of evaluators. See evaluators for 77 | // behavior details. 78 | type messageEvaluators []messageEvaluator 79 | 80 | func (m messageEvaluators) Evaluate(val protoreflect.Value, cfg *validationConfig) error { 81 | return m.EvaluateMessage(val.Message(), cfg) 82 | } 83 | 84 | func (m messageEvaluators) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) (err error) { 85 | var ok bool 86 | for _, eval := range m { 87 | evalErr := eval.EvaluateMessage(msg, cfg) 88 | if ok, err = mergeViolations(err, evalErr, cfg); !ok { 89 | return err 90 | } 91 | } 92 | return err 93 | } 94 | 95 | func (m messageEvaluators) Tautology() bool { 96 | for _, eval := range m { 97 | if !eval.Tautology() { 98 | return false 99 | } 100 | } 101 | return true 102 | } 103 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | //nolint:gochecknoglobals 24 | var ( 25 | requiredRuleDescriptor = (&validate.FieldRules{}).ProtoReflect().Descriptor().Fields().ByName("required") 26 | requiredRulePath = &validate.FieldPath{ 27 | Elements: []*validate.FieldPathElement{ 28 | fieldPathElement(requiredRuleDescriptor), 29 | }, 30 | } 31 | ) 32 | 33 | // field performs validation on a single message field, defined by its 34 | // descriptor. 35 | type field struct { 36 | // Value is the evaluator to apply to the field's value 37 | Value value 38 | // Required indicates that the field must have a set value. 39 | Required bool 40 | // HasPresence reports whether the field distinguishes between unpopulated 41 | // and default values. 42 | HasPresence bool 43 | // Whether validation should be ignored for certain conditions. 44 | Ignore validate.Ignore 45 | // Zero is the default or zero-value for this value's type 46 | Zero protoreflect.Value 47 | // Err stores if there was a compilation error constructing this evaluator. It is stored 48 | // here so that it can be returned as part of validating this specific field. 49 | Err error 50 | } 51 | 52 | // shouldIgnoreAlways returns whether this field should always skip validation. 53 | // If true, this will take precedence and all checks are skipped. 54 | func (f field) shouldIgnoreAlways() bool { 55 | return f.Ignore == validate.Ignore_IGNORE_ALWAYS 56 | } 57 | 58 | // shouldIgnoreEmpty returns whether this field should skip validation on its zero value. 59 | // This field is generally true for nullable fields or fields with the 60 | // ignore_empty rule explicitly set. 61 | func (f field) shouldIgnoreEmpty() bool { 62 | return f.HasPresence || f.Ignore == validate.Ignore_IGNORE_IF_UNPOPULATED || f.Ignore == validate.Ignore_IGNORE_IF_DEFAULT_VALUE 63 | } 64 | 65 | // shouldIgnoreDefault returns whether this field should skip validation on its zero value, 66 | // including for fields which have field presence and are set to the zero value. 67 | func (f field) shouldIgnoreDefault() bool { 68 | return f.HasPresence && f.Ignore == validate.Ignore_IGNORE_IF_DEFAULT_VALUE 69 | } 70 | 71 | func (f field) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 72 | return f.EvaluateMessage(val.Message(), cfg) 73 | } 74 | 75 | func (f field) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) (err error) { 76 | if f.shouldIgnoreAlways() { 77 | return nil 78 | } 79 | if !cfg.filter.ShouldValidate(msg, f.Value.Descriptor) { 80 | return nil 81 | } 82 | 83 | if f.Err != nil { 84 | return f.Err 85 | } 86 | 87 | if f.Required && !msg.Has(f.Value.Descriptor) { 88 | return &ValidationError{Violations: []*Violation{{ 89 | Proto: &validate.Violation{ 90 | Field: fieldPath(f.Value.Descriptor), 91 | Rule: prefixRulePath(f.Value.NestedRule, requiredRulePath), 92 | RuleId: proto.String("required"), 93 | Message: proto.String("value is required"), 94 | }, 95 | FieldValue: protoreflect.Value{}, 96 | FieldDescriptor: f.Value.Descriptor, 97 | RuleValue: protoreflect.ValueOfBool(true), 98 | RuleDescriptor: requiredRuleDescriptor, 99 | }}} 100 | } 101 | 102 | if f.shouldIgnoreEmpty() && !msg.Has(f.Value.Descriptor) { 103 | return nil 104 | } 105 | 106 | val := msg.Get(f.Value.Descriptor) 107 | if f.shouldIgnoreDefault() && val.Equal(f.Zero) { 108 | return nil 109 | } 110 | return f.Value.EvaluateField(msg, val, cfg, true) 111 | } 112 | 113 | func (f field) Tautology() bool { 114 | return !f.Required && f.Value.Tautology() && f.Err == nil 115 | } 116 | 117 | var _ messageEvaluator = field{} 118 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import "google.golang.org/protobuf/reflect/protoreflect" 18 | 19 | // The Filter interface determines which rules should be validated. 20 | type Filter interface { 21 | // ShouldValidate returns whether rules for a given message, field, or 22 | // oneof should be evaluated. For a message or oneof, this only determines 23 | // whether message-level or oneof-level rules should be evaluated, and 24 | // ShouldValidate will still be called for each field in the message. If 25 | // ShouldValidate returns false for a specific field, all rules nested 26 | // in submessages of that field will be skipped as well. 27 | // For a message, the message argument provides the message itself. For a 28 | // field or oneof, the message argument provides the containing message. 29 | ShouldValidate(message protoreflect.Message, descriptor protoreflect.Descriptor) bool 30 | } 31 | 32 | // FilterFunc is a function type that implements the Filter interface, as a 33 | // convenience for simple filters. A FilterFunc should follow the same semantics 34 | // as the ShouldValidate method of Filter. 35 | type FilterFunc func(protoreflect.Message, protoreflect.Descriptor) bool 36 | 37 | func (f FilterFunc) ShouldValidate( 38 | message protoreflect.Message, 39 | descriptor protoreflect.Descriptor, 40 | ) bool { 41 | return f(message, descriptor) 42 | } 43 | 44 | type nopFilter struct{} 45 | 46 | func (nopFilter) ShouldValidate(_ protoreflect.Message, _ protoreflect.Descriptor) bool { 47 | return true 48 | } 49 | 50 | var _ Filter = nopFilter{} 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module buf.build/go/protovalidate 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-00000000000000-c7344d9f5dae.1 7 | github.com/google/cel-go v0.25.0 8 | github.com/stretchr/testify v1.10.0 9 | google.golang.org/protobuf v1.36.6 10 | ) 11 | 12 | require ( 13 | cel.dev/expr v0.23.1 // indirect 14 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/kr/pretty v0.1.0 // indirect 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/stoewer/go-strcase v1.3.0 // indirect 20 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect 21 | golang.org/x/text v0.23.0 // indirect 22 | google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect 23 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect 24 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-00000000000000-c7344d9f5dae.1 h1:nmsl+LeZt89FPjFhJvd0LTaUjmLy4MDAC+XEbzXc3xU= 2 | buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-00000000000000-c7344d9f5dae.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= 3 | cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= 4 | cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 5 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 6 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= 12 | github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 19 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 20 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= 24 | github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 27 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 28 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 29 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 30 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 31 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 32 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= 34 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 35 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 36 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 37 | google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= 38 | google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= 39 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= 40 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 41 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 42 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 45 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 48 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | -------------------------------------------------------------------------------- /internal/cmd/protovalidate-conformance-go/README.md: -------------------------------------------------------------------------------- 1 | [![The Buf logo](../../../.github/buf-logo.svg)][buf] 2 | 3 | # Go conformance executor 4 | 5 | This binary is the [conformance testing executor](https://github.com/bufbuild/protovalidate/tree/main/tools/protovalidate-conformance) for the Go implementation. From the root of the project, the Go conformance tests can be executed with make: 6 | 7 | ```shell 8 | make conformance # runs all conformance tests 9 | 10 | make conformance ARGS='-suite uint64' # pass flags to the conformance harness 11 | ``` 12 | 13 | [buf]: https://buf.build 14 | -------------------------------------------------------------------------------- /internal/cmd/protovalidate-conformance-go/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 main 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "log" 21 | "os" 22 | "strings" 23 | 24 | "buf.build/go/protovalidate" 25 | "buf.build/go/protovalidate/internal/gen/buf/validate/conformance/harness" 26 | "google.golang.org/protobuf/proto" 27 | "google.golang.org/protobuf/reflect/protodesc" 28 | "google.golang.org/protobuf/reflect/protoreflect" 29 | "google.golang.org/protobuf/reflect/protoregistry" 30 | "google.golang.org/protobuf/types/dynamicpb" 31 | "google.golang.org/protobuf/types/known/anypb" 32 | ) 33 | 34 | func main() { 35 | log.SetFlags(0) 36 | log.SetPrefix("[protovalidate-go] ") 37 | 38 | req := &harness.TestConformanceRequest{} 39 | if data, err := io.ReadAll(os.Stdin); err != nil { 40 | log.Fatalf("failed to read input from stdin: %v", err) 41 | } else if err = proto.Unmarshal(data, req); err != nil { 42 | log.Fatalf("failed to unmarshal conformance request: %v", err) 43 | } 44 | 45 | resp, err := TestConformance(req) 46 | if err != nil { 47 | log.Fatalf("unable to test conformance: %v", err) 48 | } else if data, err := proto.Marshal(resp); err != nil { 49 | log.Fatalf("unable to marshal conformance response: %v", err) 50 | } else if _, err = os.Stdout.Write(data); err != nil { 51 | log.Fatalf("unable to write output to stdout: %v", err) 52 | } 53 | } 54 | 55 | func TestConformance(req *harness.TestConformanceRequest) (*harness.TestConformanceResponse, error) { 56 | files, err := protodesc.NewFiles(req.GetFdset()) 57 | if err != nil { 58 | err = fmt.Errorf("failed to parse file descriptors: %w", err) 59 | return nil, err 60 | } 61 | registry := &protoregistry.Types{} 62 | files.RangeFiles(func(file protoreflect.FileDescriptor) bool { 63 | for i := range file.Extensions().Len() { 64 | if err = registry.RegisterExtension( 65 | dynamicpb.NewExtensionType(file.Extensions().Get(i)), 66 | ); err != nil { 67 | return false 68 | } 69 | } 70 | return err == nil 71 | }) 72 | if err != nil { 73 | return nil, err 74 | } 75 | val, err := protovalidate.New(protovalidate.WithExtensionTypeResolver(registry)) 76 | if err != nil { 77 | err = fmt.Errorf("failed to initialize validator: %w", err) 78 | return nil, err 79 | } 80 | resp := &harness.TestConformanceResponse{Results: map[string]*harness.TestResult{}} 81 | for caseName, testCase := range req.GetCases() { 82 | resp.Results[caseName] = TestCase(val, files, testCase) 83 | } 84 | return resp, nil 85 | } 86 | 87 | func TestCase(val protovalidate.Validator, files *protoregistry.Files, testCase *anypb.Any) *harness.TestResult { 88 | urlParts := strings.Split(testCase.GetTypeUrl(), "/") 89 | fullName := protoreflect.FullName(urlParts[len(urlParts)-1]) 90 | desc, err := files.FindDescriptorByName(fullName) 91 | if err != nil { 92 | return unexpectedErrorResult("unable to find descriptor: %v", err) 93 | } 94 | msgDesc, ok := desc.(protoreflect.MessageDescriptor) 95 | if !ok { 96 | return unexpectedErrorResult("expected message descriptor, got %T", desc) 97 | } 98 | 99 | dyn := dynamicpb.NewMessage(msgDesc) 100 | if err = anypb.UnmarshalTo(testCase, dyn, proto.UnmarshalOptions{}); err != nil { 101 | return unexpectedErrorResult("unable to unmarshal test case: %v", err) 102 | } 103 | 104 | err = val.Validate(dyn) 105 | if err == nil { 106 | return &harness.TestResult{ 107 | Result: &harness.TestResult_Success{ 108 | Success: true, 109 | }, 110 | } 111 | } 112 | switch res := err.(type) { 113 | case *protovalidate.ValidationError: 114 | return &harness.TestResult{ 115 | Result: &harness.TestResult_ValidationError{ 116 | ValidationError: res.ToProto(), 117 | }, 118 | } 119 | case *protovalidate.RuntimeError: 120 | return &harness.TestResult{ 121 | Result: &harness.TestResult_RuntimeError{ 122 | RuntimeError: res.Error(), 123 | }, 124 | } 125 | case *protovalidate.CompilationError: 126 | return &harness.TestResult{ 127 | Result: &harness.TestResult_CompilationError{ 128 | CompilationError: res.Error(), 129 | }, 130 | } 131 | default: 132 | return unexpectedErrorResult("unknown error: %v", err) 133 | } 134 | } 135 | 136 | func unexpectedErrorResult(format string, args ...any) *harness.TestResult { 137 | return &harness.TestResult{ 138 | Result: &harness.TestResult_UnexpectedError{ 139 | UnexpectedError: fmt.Sprintf(format, args...), 140 | }, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /internal/gen/buf/validate/conformance/cases/filename-with-dash.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.36.6 18 | // protoc (unknown) 19 | // source: buf/validate/conformance/cases/filename-with-dash.proto 20 | 21 | //go:build !protoopaque 22 | 23 | package cases 24 | 25 | import ( 26 | _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 27 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 28 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 29 | reflect "reflect" 30 | unsafe "unsafe" 31 | ) 32 | 33 | const ( 34 | // Verify that this generated code is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 36 | // Verify that runtime/protoimpl is sufficiently up-to-date. 37 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 38 | ) 39 | 40 | var File_buf_validate_conformance_cases_filename_with_dash_proto protoreflect.FileDescriptor 41 | 42 | const file_buf_validate_conformance_cases_filename_with_dash_proto_rawDesc = "" + 43 | "\n" + 44 | "7buf/validate/conformance/cases/filename-with-dash.proto\x12\x1ebuf.validate.conformance.cases\x1a\x1bbuf/validate/validate.protoB\x9f\x02\n" + 45 | "\"com.buf.validate.conformance.casesB\x15FilenameWithDashProtoP\x01ZFbuf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases\xa2\x02\x04BVCC\xaa\x02\x1eBuf.Validate.Conformance.Cases\xca\x02\x1eBuf\\Validate\\Conformance\\Cases\xe2\x02*Buf\\Validate\\Conformance\\Cases\\GPBMetadata\xea\x02!Buf::Validate::Conformance::Casesb\x06proto3" 46 | 47 | var file_buf_validate_conformance_cases_filename_with_dash_proto_goTypes = []any{} 48 | var file_buf_validate_conformance_cases_filename_with_dash_proto_depIdxs = []int32{ 49 | 0, // [0:0] is the sub-list for method output_type 50 | 0, // [0:0] is the sub-list for method input_type 51 | 0, // [0:0] is the sub-list for extension type_name 52 | 0, // [0:0] is the sub-list for extension extendee 53 | 0, // [0:0] is the sub-list for field type_name 54 | } 55 | 56 | func init() { file_buf_validate_conformance_cases_filename_with_dash_proto_init() } 57 | func file_buf_validate_conformance_cases_filename_with_dash_proto_init() { 58 | if File_buf_validate_conformance_cases_filename_with_dash_proto != nil { 59 | return 60 | } 61 | type x struct{} 62 | out := protoimpl.TypeBuilder{ 63 | File: protoimpl.DescBuilder{ 64 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 65 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_validate_conformance_cases_filename_with_dash_proto_rawDesc), len(file_buf_validate_conformance_cases_filename_with_dash_proto_rawDesc)), 66 | NumEnums: 0, 67 | NumMessages: 0, 68 | NumExtensions: 0, 69 | NumServices: 0, 70 | }, 71 | GoTypes: file_buf_validate_conformance_cases_filename_with_dash_proto_goTypes, 72 | DependencyIndexes: file_buf_validate_conformance_cases_filename_with_dash_proto_depIdxs, 73 | }.Build() 74 | File_buf_validate_conformance_cases_filename_with_dash_proto = out.File 75 | file_buf_validate_conformance_cases_filename_with_dash_proto_goTypes = nil 76 | file_buf_validate_conformance_cases_filename_with_dash_proto_depIdxs = nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/gen/buf/validate/conformance/cases/filename-with-dash_protoopaque.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.36.6 18 | // protoc (unknown) 19 | // source: buf/validate/conformance/cases/filename-with-dash.proto 20 | 21 | //go:build protoopaque 22 | 23 | package cases 24 | 25 | import ( 26 | _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 27 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 28 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 29 | reflect "reflect" 30 | unsafe "unsafe" 31 | ) 32 | 33 | const ( 34 | // Verify that this generated code is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 36 | // Verify that runtime/protoimpl is sufficiently up-to-date. 37 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 38 | ) 39 | 40 | var File_buf_validate_conformance_cases_filename_with_dash_proto protoreflect.FileDescriptor 41 | 42 | const file_buf_validate_conformance_cases_filename_with_dash_proto_rawDesc = "" + 43 | "\n" + 44 | "7buf/validate/conformance/cases/filename-with-dash.proto\x12\x1ebuf.validate.conformance.cases\x1a\x1bbuf/validate/validate.protoB\x9f\x02\n" + 45 | "\"com.buf.validate.conformance.casesB\x15FilenameWithDashProtoP\x01ZFbuf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases\xa2\x02\x04BVCC\xaa\x02\x1eBuf.Validate.Conformance.Cases\xca\x02\x1eBuf\\Validate\\Conformance\\Cases\xe2\x02*Buf\\Validate\\Conformance\\Cases\\GPBMetadata\xea\x02!Buf::Validate::Conformance::Casesb\x06proto3" 46 | 47 | var file_buf_validate_conformance_cases_filename_with_dash_proto_goTypes = []any{} 48 | var file_buf_validate_conformance_cases_filename_with_dash_proto_depIdxs = []int32{ 49 | 0, // [0:0] is the sub-list for method output_type 50 | 0, // [0:0] is the sub-list for method input_type 51 | 0, // [0:0] is the sub-list for extension type_name 52 | 0, // [0:0] is the sub-list for extension extendee 53 | 0, // [0:0] is the sub-list for field type_name 54 | } 55 | 56 | func init() { file_buf_validate_conformance_cases_filename_with_dash_proto_init() } 57 | func file_buf_validate_conformance_cases_filename_with_dash_proto_init() { 58 | if File_buf_validate_conformance_cases_filename_with_dash_proto != nil { 59 | return 60 | } 61 | type x struct{} 62 | out := protoimpl.TypeBuilder{ 63 | File: protoimpl.DescBuilder{ 64 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 65 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_validate_conformance_cases_filename_with_dash_proto_rawDesc), len(file_buf_validate_conformance_cases_filename_with_dash_proto_rawDesc)), 66 | NumEnums: 0, 67 | NumMessages: 0, 68 | NumExtensions: 0, 69 | NumServices: 0, 70 | }, 71 | GoTypes: file_buf_validate_conformance_cases_filename_with_dash_proto_goTypes, 72 | DependencyIndexes: file_buf_validate_conformance_cases_filename_with_dash_proto_depIdxs, 73 | }.Build() 74 | File_buf_validate_conformance_cases_filename_with_dash_proto = out.File 75 | file_buf_validate_conformance_cases_filename_with_dash_proto_goTypes = nil 76 | file_buf_validate_conformance_cases_filename_with_dash_proto_depIdxs = nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/gen/buf/validate/conformance/cases/subdirectory/in_subdirectory.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.36.6 18 | // protoc (unknown) 19 | // source: buf/validate/conformance/cases/subdirectory/in_subdirectory.proto 20 | 21 | //go:build !protoopaque 22 | 23 | package subdirectory 24 | 25 | import ( 26 | _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 27 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 28 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 29 | reflect "reflect" 30 | unsafe "unsafe" 31 | ) 32 | 33 | const ( 34 | // Verify that this generated code is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 36 | // Verify that runtime/protoimpl is sufficiently up-to-date. 37 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 38 | ) 39 | 40 | var File_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto protoreflect.FileDescriptor 41 | 42 | const file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_rawDesc = "" + 43 | "\n" + 44 | "Abuf/validate/conformance/cases/subdirectory/in_subdirectory.proto\x12+buf.validate.conformance.cases.subdirectory\x1a\x1bbuf/validate/validate.protoB\xed\x02\n" + 45 | "/com.buf.validate.conformance.cases.subdirectoryB\x13InSubdirectoryProtoP\x01ZSbuf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases/subdirectory\xa2\x02\x05BVCCS\xaa\x02+Buf.Validate.Conformance.Cases.Subdirectory\xca\x02+Buf\\Validate\\Conformance\\Cases\\Subdirectory\xe2\x027Buf\\Validate\\Conformance\\Cases\\Subdirectory\\GPBMetadata\xea\x02/Buf::Validate::Conformance::Cases::Subdirectoryb\x06proto3" 46 | 47 | var file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_goTypes = []any{} 48 | var file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_depIdxs = []int32{ 49 | 0, // [0:0] is the sub-list for method output_type 50 | 0, // [0:0] is the sub-list for method input_type 51 | 0, // [0:0] is the sub-list for extension type_name 52 | 0, // [0:0] is the sub-list for extension extendee 53 | 0, // [0:0] is the sub-list for field type_name 54 | } 55 | 56 | func init() { file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_init() } 57 | func file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_init() { 58 | if File_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto != nil { 59 | return 60 | } 61 | type x struct{} 62 | out := protoimpl.TypeBuilder{ 63 | File: protoimpl.DescBuilder{ 64 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 65 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_rawDesc), len(file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_rawDesc)), 66 | NumEnums: 0, 67 | NumMessages: 0, 68 | NumExtensions: 0, 69 | NumServices: 0, 70 | }, 71 | GoTypes: file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_goTypes, 72 | DependencyIndexes: file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_depIdxs, 73 | }.Build() 74 | File_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto = out.File 75 | file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_goTypes = nil 76 | file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_depIdxs = nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/gen/buf/validate/conformance/cases/subdirectory/in_subdirectory_protoopaque.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // versions: 17 | // protoc-gen-go v1.36.6 18 | // protoc (unknown) 19 | // source: buf/validate/conformance/cases/subdirectory/in_subdirectory.proto 20 | 21 | //go:build protoopaque 22 | 23 | package subdirectory 24 | 25 | import ( 26 | _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 27 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 28 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 29 | reflect "reflect" 30 | unsafe "unsafe" 31 | ) 32 | 33 | const ( 34 | // Verify that this generated code is sufficiently up-to-date. 35 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 36 | // Verify that runtime/protoimpl is sufficiently up-to-date. 37 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 38 | ) 39 | 40 | var File_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto protoreflect.FileDescriptor 41 | 42 | const file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_rawDesc = "" + 43 | "\n" + 44 | "Abuf/validate/conformance/cases/subdirectory/in_subdirectory.proto\x12+buf.validate.conformance.cases.subdirectory\x1a\x1bbuf/validate/validate.protoB\xed\x02\n" + 45 | "/com.buf.validate.conformance.cases.subdirectoryB\x13InSubdirectoryProtoP\x01ZSbuf.build/go/protovalidate/internal/gen/buf/validate/conformance/cases/subdirectory\xa2\x02\x05BVCCS\xaa\x02+Buf.Validate.Conformance.Cases.Subdirectory\xca\x02+Buf\\Validate\\Conformance\\Cases\\Subdirectory\xe2\x027Buf\\Validate\\Conformance\\Cases\\Subdirectory\\GPBMetadata\xea\x02/Buf::Validate::Conformance::Cases::Subdirectoryb\x06proto3" 46 | 47 | var file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_goTypes = []any{} 48 | var file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_depIdxs = []int32{ 49 | 0, // [0:0] is the sub-list for method output_type 50 | 0, // [0:0] is the sub-list for method input_type 51 | 0, // [0:0] is the sub-list for extension type_name 52 | 0, // [0:0] is the sub-list for extension extendee 53 | 0, // [0:0] is the sub-list for field type_name 54 | } 55 | 56 | func init() { file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_init() } 57 | func file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_init() { 58 | if File_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto != nil { 59 | return 60 | } 61 | type x struct{} 62 | out := protoimpl.TypeBuilder{ 63 | File: protoimpl.DescBuilder{ 64 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 65 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_rawDesc), len(file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_rawDesc)), 66 | NumEnums: 0, 67 | NumMessages: 0, 68 | NumExtensions: 0, 69 | NumServices: 0, 70 | }, 71 | GoTypes: file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_goTypes, 72 | DependencyIndexes: file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_depIdxs, 73 | }.Build() 74 | File_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto = out.File 75 | file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_goTypes = nil 76 | file_buf_validate_conformance_cases_subdirectory_in_subdirectory_proto_depIdxs = nil 77 | } 78 | -------------------------------------------------------------------------------- /lookups.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/reflect/protoreflect" 20 | ) 21 | 22 | var ( 23 | // fieldRulesDesc provides a Descriptor for validate.FieldRules. 24 | fieldRulesDesc = (*validate.FieldRules)(nil).ProtoReflect().Descriptor() 25 | 26 | // fieldRulesOneofDesc provides the OneofDescriptor for the type union 27 | // in FieldRules. 28 | fieldRulesOneofDesc = fieldRulesDesc.Oneofs().ByName("type") 29 | 30 | // mapFieldRulesDesc provides the FieldDescriptor for the map standard 31 | // rules. 32 | mapFieldRulesDesc = fieldRulesDesc.Fields().ByName("map") 33 | 34 | // repeatedFieldRulesDesc provides the FieldDescriptor for the repeated 35 | // standard rules. 36 | repeatedFieldRulesDesc = fieldRulesDesc.Fields().ByName("repeated") 37 | ) 38 | 39 | // expectedStandardRules maps protocol buffer field kinds to their 40 | // expected field rules. 41 | var expectedStandardRules = map[protoreflect.Kind]protoreflect.FieldDescriptor{ 42 | protoreflect.FloatKind: fieldRulesDesc.Fields().ByName("float"), 43 | protoreflect.DoubleKind: fieldRulesDesc.Fields().ByName("double"), 44 | protoreflect.Int32Kind: fieldRulesDesc.Fields().ByName("int32"), 45 | protoreflect.Int64Kind: fieldRulesDesc.Fields().ByName("int64"), 46 | protoreflect.Uint32Kind: fieldRulesDesc.Fields().ByName("uint32"), 47 | protoreflect.Uint64Kind: fieldRulesDesc.Fields().ByName("uint64"), 48 | protoreflect.Sint32Kind: fieldRulesDesc.Fields().ByName("sint32"), 49 | protoreflect.Sint64Kind: fieldRulesDesc.Fields().ByName("sint64"), 50 | protoreflect.Fixed32Kind: fieldRulesDesc.Fields().ByName("fixed32"), 51 | protoreflect.Fixed64Kind: fieldRulesDesc.Fields().ByName("fixed64"), 52 | protoreflect.Sfixed32Kind: fieldRulesDesc.Fields().ByName("sfixed32"), 53 | protoreflect.Sfixed64Kind: fieldRulesDesc.Fields().ByName("sfixed64"), 54 | protoreflect.BoolKind: fieldRulesDesc.Fields().ByName("bool"), 55 | protoreflect.StringKind: fieldRulesDesc.Fields().ByName("string"), 56 | protoreflect.BytesKind: fieldRulesDesc.Fields().ByName("bytes"), 57 | protoreflect.EnumKind: fieldRulesDesc.Fields().ByName("enum"), 58 | } 59 | 60 | var expectedWKTRules = map[protoreflect.FullName]protoreflect.FieldDescriptor{ 61 | "google.protobuf.Any": fieldRulesDesc.Fields().ByName("any"), 62 | "google.protobuf.Duration": fieldRulesDesc.Fields().ByName("duration"), 63 | "google.protobuf.Timestamp": fieldRulesDesc.Fields().ByName("timestamp"), 64 | } 65 | 66 | // expectedWrapperRules returns the validate.FieldRules field that 67 | // is expected for the given wrapper well-known type's full name. If ok is 68 | // false, no standard rules exist for that type. 69 | func expectedWrapperRules(fqn protoreflect.FullName) (desc protoreflect.FieldDescriptor, ok bool) { 70 | switch fqn { 71 | case "google.protobuf.BoolValue": 72 | return expectedStandardRules[protoreflect.BoolKind], true 73 | case "google.protobuf.BytesValue": 74 | return expectedStandardRules[protoreflect.BytesKind], true 75 | case "google.protobuf.DoubleValue": 76 | return expectedStandardRules[protoreflect.DoubleKind], true 77 | case "google.protobuf.FloatValue": 78 | return expectedStandardRules[protoreflect.FloatKind], true 79 | case "google.protobuf.Int32Value": 80 | return expectedStandardRules[protoreflect.Int32Kind], true 81 | case "google.protobuf.Int64Value": 82 | return expectedStandardRules[protoreflect.Int64Kind], true 83 | case "google.protobuf.StringValue": 84 | return expectedStandardRules[protoreflect.StringKind], true 85 | case "google.protobuf.UInt32Value": 86 | return expectedStandardRules[protoreflect.Uint32Kind], true 87 | case "google.protobuf.UInt64Value": 88 | return expectedStandardRules[protoreflect.Uint64Kind], true 89 | default: 90 | return nil, false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lookups_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | "google.golang.org/protobuf/proto" 22 | "google.golang.org/protobuf/reflect/protoreflect" 23 | ) 24 | 25 | func TestExpectedWrapperRules(t *testing.T) { 26 | t.Parallel() 27 | 28 | tests := map[protoreflect.FullName]*string{ 29 | "google.protobuf.BoolValue": proto.String("buf.validate.FieldRules.bool"), 30 | "google.protobuf.BytesValue": proto.String("buf.validate.FieldRules.bytes"), 31 | "google.protobuf.DoubleValue": proto.String("buf.validate.FieldRules.double"), 32 | "google.protobuf.FloatValue": proto.String("buf.validate.FieldRules.float"), 33 | "google.protobuf.Int32Value": proto.String("buf.validate.FieldRules.int32"), 34 | "google.protobuf.Int64Value": proto.String("buf.validate.FieldRules.int64"), 35 | "google.protobuf.StringValue": proto.String("buf.validate.FieldRules.string"), 36 | "google.protobuf.UInt32Value": proto.String("buf.validate.FieldRules.uint32"), 37 | "google.protobuf.UInt64Value": proto.String("buf.validate.FieldRules.uint64"), 38 | "foo.bar": nil, 39 | } 40 | 41 | for name, cons := range tests { 42 | fqn, rule := name, cons 43 | t.Run(string(fqn), func(t *testing.T) { 44 | t.Parallel() 45 | desc, ok := expectedWrapperRules(fqn) 46 | if rule != nil { 47 | assert.Equal(t, *rule, string(desc.FullName())) 48 | assert.True(t, ok) 49 | } else { 50 | assert.False(t, ok) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | "strconv" 20 | 21 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 22 | "google.golang.org/protobuf/proto" 23 | "google.golang.org/protobuf/reflect/protoreflect" 24 | "google.golang.org/protobuf/types/descriptorpb" 25 | ) 26 | 27 | //nolint:gochecknoglobals 28 | var ( 29 | mapRuleDescriptor = (&validate.FieldRules{}).ProtoReflect().Descriptor().Fields().ByName("map") 30 | mapKeysRuleDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("keys") 31 | mapKeysRulePath = &validate.FieldPath{ 32 | Elements: []*validate.FieldPathElement{ 33 | fieldPathElement(mapRuleDescriptor), 34 | fieldPathElement(mapKeysRuleDescriptor), 35 | }, 36 | } 37 | mapValuesDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("values") 38 | mapValuesRulePath = &validate.FieldPath{ 39 | Elements: []*validate.FieldPathElement{ 40 | fieldPathElement(mapRuleDescriptor), 41 | fieldPathElement(mapValuesDescriptor), 42 | }, 43 | } 44 | ) 45 | 46 | // kvPairs performs validation on a map field's KV Pairs. 47 | type kvPairs struct { 48 | base 49 | 50 | // KeyRules are checked on the map keys 51 | KeyRules value 52 | // ValueRules are checked on the map values 53 | ValueRules value 54 | } 55 | 56 | func newKVPairs(valEval *value) kvPairs { 57 | return kvPairs{ 58 | base: newBase(valEval), 59 | KeyRules: value{NestedRule: mapKeysRulePath}, 60 | ValueRules: value{NestedRule: mapValuesRulePath}, 61 | } 62 | } 63 | 64 | func (m kvPairs) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) (err error) { 65 | var ok bool 66 | val.Map().Range(func(key protoreflect.MapKey, value protoreflect.Value) bool { 67 | evalErr := m.evalPairs(msg, key, value, cfg) 68 | if evalErr != nil { 69 | element := &validate.FieldPathElement{ 70 | FieldNumber: proto.Int32(m.FieldPathElement.GetFieldNumber()), 71 | FieldType: m.base.FieldPathElement.GetFieldType().Enum(), 72 | FieldName: proto.String(m.FieldPathElement.GetFieldName()), 73 | } 74 | element.KeyType = descriptorpb.FieldDescriptorProto_Type(m.base.Descriptor.MapKey().Kind()).Enum() 75 | element.ValueType = descriptorpb.FieldDescriptorProto_Type(m.base.Descriptor.MapValue().Kind()).Enum() 76 | switch m.base.Descriptor.MapKey().Kind() { 77 | case protoreflect.BoolKind: 78 | element.Subscript = &validate.FieldPathElement_BoolKey{BoolKey: key.Bool()} 79 | case protoreflect.Int32Kind, protoreflect.Int64Kind, 80 | protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind, 81 | protoreflect.Sint32Kind, protoreflect.Sint64Kind: 82 | element.Subscript = &validate.FieldPathElement_IntKey{IntKey: key.Int()} 83 | case protoreflect.Uint32Kind, protoreflect.Uint64Kind, 84 | protoreflect.Fixed32Kind, protoreflect.Fixed64Kind: 85 | element.Subscript = &validate.FieldPathElement_UintKey{UintKey: key.Uint()} 86 | case protoreflect.StringKind: 87 | element.Subscript = &validate.FieldPathElement_StringKey{StringKey: key.String()} 88 | case protoreflect.EnumKind, protoreflect.FloatKind, protoreflect.DoubleKind, 89 | protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind: 90 | fallthrough 91 | default: 92 | err = &CompilationError{cause: fmt.Errorf( 93 | "unexpected map key type %s", 94 | m.base.Descriptor.MapKey().Kind())} 95 | return false 96 | } 97 | updateViolationPaths(evalErr, element, m.RulePrefix.GetElements()) 98 | } 99 | ok, err = mergeViolations(err, evalErr, cfg) 100 | return ok 101 | }) 102 | return err 103 | } 104 | 105 | func (m kvPairs) evalPairs(msg protoreflect.Message, key protoreflect.MapKey, value protoreflect.Value, cfg *validationConfig) (err error) { 106 | evalErr := m.KeyRules.EvaluateField(msg, key.Value(), cfg, true) 107 | markViolationForKey(evalErr) 108 | ok, err := mergeViolations(err, evalErr, cfg) 109 | if !ok { 110 | return err 111 | } 112 | 113 | evalErr = m.ValueRules.EvaluateField(msg, value, cfg, true) 114 | _, err = mergeViolations(err, evalErr, cfg) 115 | return err 116 | } 117 | 118 | func (m kvPairs) Tautology() bool { 119 | return m.KeyRules.Tautology() && 120 | m.ValueRules.Tautology() 121 | } 122 | 123 | func (m kvPairs) formatKey(key any) string { 124 | switch k := key.(type) { 125 | case string: 126 | return strconv.Quote(k) 127 | default: 128 | return fmt.Sprintf("%v", key) 129 | } 130 | } 131 | 132 | var _ evaluator = kvPairs{} 133 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestFormatKey(t *testing.T) { 24 | t.Parallel() 25 | tests := []struct { 26 | key any 27 | expected string 28 | }{ 29 | { 30 | key: int32(32), 31 | expected: "32", 32 | }, 33 | { 34 | key: int64(64), 35 | expected: "64", 36 | }, 37 | { 38 | key: uint32(32), 39 | expected: "32", 40 | }, 41 | { 42 | key: uint32(64), 43 | expected: "64", 44 | }, 45 | { 46 | key: true, 47 | expected: "true", 48 | }, 49 | { 50 | key: false, 51 | expected: "false", 52 | }, 53 | { 54 | key: `"foobar"`, 55 | expected: `"\"foobar\""`, 56 | }, 57 | } 58 | 59 | kv := kvPairs{} 60 | for _, tc := range tests { 61 | test := tc 62 | t.Run(test.expected, func(t *testing.T) { 63 | t.Parallel() 64 | actual := kv.formatKey(test.key) 65 | assert.Equal(t, test.expected, actual) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | // message performs validation on a protoreflect.Message. 24 | type message struct { 25 | // Err stores if there was a compilation error constructing this evaluator. 26 | // It is cached here so that it can be stored in the registry's lookup table. 27 | Err error 28 | 29 | // evaluators are the individual evaluators that are applied to a message. 30 | evaluators messageEvaluators 31 | 32 | // nestedEvaluators are the evaluators that are applied to nested fields and 33 | // oneofs. 34 | nestedEvaluators messageEvaluators 35 | } 36 | 37 | func (m *message) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 38 | return m.EvaluateMessage(val.Message(), cfg) 39 | } 40 | 41 | func (m *message) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { 42 | var ( 43 | err error 44 | ok bool 45 | ) 46 | if cfg.filter.ShouldValidate(msg, msg.Descriptor()) { 47 | if m.Err != nil { 48 | return m.Err 49 | } 50 | if ok, err = mergeViolations(err, m.evaluators.EvaluateMessage(msg, cfg), cfg); !ok { 51 | return err 52 | } 53 | } 54 | _, err = mergeViolations(err, m.nestedEvaluators.EvaluateMessage(msg, cfg), cfg) 55 | return err 56 | } 57 | 58 | func (m *message) Tautology() bool { 59 | // returning false for now to avoid recursive messages causing false positives 60 | // on tautology detection. 61 | // 62 | // TODO: use a more sophisticated method to detect recursions so we can 63 | // continue to detect tautologies on message evaluators. 64 | return false 65 | } 66 | 67 | func (m *message) Append(eval messageEvaluator) { 68 | if eval != nil && !eval.Tautology() { 69 | m.evaluators = append(m.evaluators, eval) 70 | } 71 | } 72 | 73 | func (m *message) AppendNested(eval messageEvaluator) { 74 | if eval != nil && !eval.Tautology() { 75 | m.nestedEvaluators = append(m.nestedEvaluators, eval) 76 | } 77 | } 78 | 79 | // unknownMessage is a MessageEvaluator for an unknown descriptor. This is 80 | // returned only if lazy-building of evaluators has been disabled and an unknown 81 | // descriptor is encountered. 82 | type unknownMessage struct { 83 | desc protoreflect.MessageDescriptor 84 | } 85 | 86 | func (u unknownMessage) Err() error { 87 | return &CompilationError{cause: fmt.Errorf( 88 | "no evaluator available for %s", 89 | u.desc.FullName())} 90 | } 91 | 92 | func (u unknownMessage) Tautology() bool { return false } 93 | 94 | func (u unknownMessage) Evaluate(_ protoreflect.Message, _ protoreflect.Value, _ *validationConfig) error { 95 | return u.Err() 96 | } 97 | 98 | func (u unknownMessage) EvaluateMessage(_ protoreflect.Message, _ *validationConfig) error { 99 | return u.Err() 100 | } 101 | 102 | // embeddedMessage is a wrapper for fields containing messages. It contains data that 103 | // may differ per embeddedMessage message so that it is not cached. 104 | type embeddedMessage struct { 105 | base 106 | 107 | message *message 108 | } 109 | 110 | func (m *embeddedMessage) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 111 | err := m.message.EvaluateMessage(val.Message(), cfg) 112 | updateViolationPaths(err, m.FieldPathElement, nil) 113 | return err 114 | } 115 | 116 | func (m *embeddedMessage) Tautology() bool { 117 | return m.message.Tautology() 118 | } 119 | 120 | var ( 121 | _ messageEvaluator = (*message)(nil) 122 | _ messageEvaluator = (*unknownMessage)(nil) 123 | _ evaluator = (*embeddedMessage)(nil) 124 | ) 125 | -------------------------------------------------------------------------------- /oneof.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | // oneof performs validation on a oneof union. 24 | type oneof struct { 25 | // Descriptor is the OneofDescriptor targeted by this evaluator 26 | Descriptor protoreflect.OneofDescriptor 27 | // Required indicates that a member of the oneof must be set 28 | Required bool 29 | } 30 | 31 | func (o oneof) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 32 | return o.EvaluateMessage(val.Message(), cfg) 33 | } 34 | 35 | func (o oneof) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { 36 | if !cfg.filter.ShouldValidate(msg, o.Descriptor) || 37 | !o.Required || msg.WhichOneof(o.Descriptor) != nil { 38 | return nil 39 | } 40 | return &ValidationError{Violations: []*Violation{{ 41 | Proto: &validate.Violation{ 42 | Field: &validate.FieldPath{ 43 | Elements: []*validate.FieldPathElement{{ 44 | FieldName: proto.String(string(o.Descriptor.Name())), 45 | }}, 46 | }, 47 | RuleId: proto.String("required"), 48 | Message: proto.String("exactly one field is required in oneof"), 49 | }, 50 | }}} 51 | } 52 | 53 | func (o oneof) Tautology() bool { 54 | return !o.Required 55 | } 56 | 57 | var _ messageEvaluator = oneof{} 58 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "google.golang.org/protobuf/proto" 19 | "google.golang.org/protobuf/reflect/protoreflect" 20 | "google.golang.org/protobuf/reflect/protoregistry" 21 | "google.golang.org/protobuf/types/known/timestamppb" 22 | ) 23 | 24 | // A ValidatorOption modifies the default configuration of a Validator. See the 25 | // individual options for their defaults and affects on the fallibility of 26 | // configuring a Validator. 27 | type ValidatorOption interface { 28 | applyToValidator(cfg *config) 29 | } 30 | 31 | // WithMessages allows warming up the Validator with messages that are 32 | // expected to be validated. Messages included transitively (i.e., fields with 33 | // message values) are automatically handled. 34 | func WithMessages(messages ...proto.Message) ValidatorOption { 35 | desc := make([]protoreflect.MessageDescriptor, len(messages)) 36 | for i, msg := range messages { 37 | desc[i] = msg.ProtoReflect().Descriptor() 38 | } 39 | return WithMessageDescriptors(desc...) 40 | } 41 | 42 | // WithMessageDescriptors allows warming up the Validator with message 43 | // descriptors that are expected to be validated. Messages included transitively 44 | // (i.e., fields with message values) are automatically handled. 45 | func WithMessageDescriptors(descriptors ...protoreflect.MessageDescriptor) ValidatorOption { 46 | return &messageDescriptorsOption{descriptors} 47 | } 48 | 49 | // WithDisableLazy prevents the Validator from lazily building validation logic 50 | // for a message it has not encountered before. Disabling lazy logic 51 | // additionally eliminates any internal locking as the validator becomes 52 | // read-only. 53 | // 54 | // Note: All expected messages must be provided by WithMessages or 55 | // WithMessageDescriptors during initialization. 56 | func WithDisableLazy() ValidatorOption { 57 | return &disableLazyOption{} 58 | } 59 | 60 | // WithExtensionTypeResolver specifies a resolver to use when reparsing unknown 61 | // extension types. When dealing with dynamic file descriptor sets, passing this 62 | // option will allow extensions to be resolved using a custom resolver. 63 | // 64 | // To ignore unknown extension fields, use the [WithAllowUnknownFields] option. 65 | // Note that this may result in messages being treated as valid even though not 66 | // all rules are being applied. 67 | func WithExtensionTypeResolver(extensionTypeResolver protoregistry.ExtensionTypeResolver) ValidatorOption { 68 | return &extensionTypeResolverOption{extensionTypeResolver} 69 | } 70 | 71 | // WithAllowUnknownFields specifies if the presence of unknown field rules 72 | // should cause compilation to fail with an error. When set to false, an unknown 73 | // field will simply be ignored, which will cause rules to silently not be 74 | // applied. This condition may occur if a predefined rule definition isn't 75 | // present in the extension type resolver, or when passing dynamic messages with 76 | // standard rules defined in a newer version of protovalidate. The default 77 | // value is false, to prevent silently-incorrect validation from occurring. 78 | func WithAllowUnknownFields() ValidatorOption { 79 | return &allowUnknownFieldsOption{} 80 | } 81 | 82 | // A ValidationOption specifies per-validation configuration. See the individual 83 | // options for their defaults and effects. 84 | type ValidationOption interface { 85 | applyToValidation(cfg *validationConfig) 86 | } 87 | 88 | // WithFilter specifies a filter to use for this validation. A filter can 89 | // control which fields are evaluated by the validator. 90 | func WithFilter(filter Filter) ValidationOption { 91 | return &filterOption{filter} 92 | } 93 | 94 | // Option implements both [ValidatorOption] and [ValidationOption], so it can be 95 | // applied both to validator instances as well as individual validations. 96 | type Option interface { 97 | ValidatorOption 98 | ValidationOption 99 | } 100 | 101 | // WithFailFast specifies whether validation should fail on the first rule 102 | // violation encountered or if all violations should be accumulated. By default, 103 | // all violations are accumulated. 104 | func WithFailFast() Option { 105 | return &failFastOption{} 106 | } 107 | 108 | // WithNowFunc specifies the function used to derive the `now` variable in CEL 109 | // expressions. By default, [timestamppb.Now] is used. 110 | func WithNowFunc(fn func() *timestamppb.Timestamp) Option { 111 | return nowFuncOption(fn) 112 | } 113 | 114 | type messageDescriptorsOption struct { 115 | descriptors []protoreflect.MessageDescriptor 116 | } 117 | 118 | func (o *messageDescriptorsOption) applyToValidator(cfg *config) { 119 | cfg.desc = append(cfg.desc, o.descriptors...) 120 | } 121 | 122 | type disableLazyOption struct{} 123 | 124 | func (o *disableLazyOption) applyToValidator(cfg *config) { 125 | cfg.disableLazy = true 126 | } 127 | 128 | type extensionTypeResolverOption struct { 129 | extensionTypeResolver protoregistry.ExtensionTypeResolver 130 | } 131 | 132 | func (o *extensionTypeResolverOption) applyToValidator(cfg *config) { 133 | cfg.extensionTypeResolver = o.extensionTypeResolver 134 | } 135 | 136 | type allowUnknownFieldsOption struct{} 137 | 138 | func (o *allowUnknownFieldsOption) applyToValidator(cfg *config) { 139 | cfg.allowUnknownFields = true 140 | } 141 | 142 | type filterOption struct{ filter Filter } 143 | 144 | func (o *filterOption) applyToValidation(cfg *validationConfig) { 145 | if o.filter == nil { 146 | cfg.filter = nopFilter{} 147 | } else { 148 | cfg.filter = o.filter 149 | } 150 | } 151 | 152 | type failFastOption struct{} 153 | 154 | func (o *failFastOption) applyToValidator(cfg *config) { 155 | cfg.failFast = true 156 | } 157 | 158 | func (o *failFastOption) applyToValidation(cfg *validationConfig) { 159 | cfg.failFast = true 160 | } 161 | 162 | type nowFuncOption func() *timestamppb.Timestamp 163 | 164 | func (o nowFuncOption) applyToValidator(cfg *config) { 165 | cfg.nowFn = o 166 | } 167 | 168 | func (o nowFuncOption) applyToValidation(cfg *validationConfig) { 169 | cfg.nowFn = o 170 | } 171 | -------------------------------------------------------------------------------- /program.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | "github.com/google/cel-go/cel" 22 | "google.golang.org/protobuf/proto" 23 | "google.golang.org/protobuf/reflect/protoreflect" 24 | ) 25 | 26 | //nolint:gochecknoglobals // amortized, eliminates allocations for all CEL programs 27 | var globalVarPool = &variablePool{New: func() any { return &variable{} }} 28 | 29 | //nolint:gochecknoglobals // amortized, eliminates allocations for all CEL programs 30 | var globalNowPool = &nowPool{New: func() any { return &now{} }} 31 | 32 | // programSet is a list of compiledProgram expressions that are evaluated 33 | // together with the same input value. All expressions in a programSet may refer 34 | // to a `this` variable. 35 | type programSet []compiledProgram 36 | 37 | // Eval applies the contained expressions to the provided `this` val, returning 38 | // either *errors.ValidationError if the input is invalid or errors.RuntimeError 39 | // if there is a type or range error. If failFast is true, execution stops at 40 | // the first failed expression. 41 | func (s programSet) Eval(val protoreflect.Value, cfg *validationConfig) error { 42 | binding := s.bindThis(val.Interface()) 43 | defer globalVarPool.Put(binding) 44 | 45 | var violations []*Violation 46 | for _, expr := range s { 47 | violation, err := expr.eval(binding, cfg) 48 | if err != nil { 49 | return err 50 | } 51 | if violation != nil { 52 | violations = append(violations, violation) 53 | if cfg.failFast { 54 | break 55 | } 56 | } 57 | } 58 | 59 | if len(violations) > 0 { 60 | return &ValidationError{Violations: violations} 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (s programSet) bindThis(val any) *variable { 67 | binding := globalVarPool.Get() 68 | binding.Name = "this" 69 | 70 | switch value := val.(type) { 71 | case protoreflect.Message: 72 | binding.Val = value.Interface() 73 | case protoreflect.Map: 74 | // TODO: expensive to create this copy, but getting this into a ref.Val with 75 | // traits.Mapper is not terribly feasible from this type. 76 | bindingVal := make(map[any]any, value.Len()) 77 | value.Range(func(key protoreflect.MapKey, value protoreflect.Value) bool { 78 | // Cel operates on 64-bit integers, so if our map type is 32-bit, we 79 | // need to widen to a 64-bit type in the binding due to our usage of 80 | // a map[any]any. 81 | switch key.Interface().(type) { 82 | case int32: 83 | bindingVal[key.Int()] = value.Interface() 84 | case uint32: 85 | bindingVal[key.Uint()] = value.Interface() 86 | default: 87 | bindingVal[key.Interface()] = value.Interface() 88 | } 89 | return true 90 | }) 91 | binding.Val = bindingVal 92 | default: 93 | binding.Val = value 94 | } 95 | 96 | return binding 97 | } 98 | 99 | // compiledProgram is a parsed and type-checked cel.Program along with the 100 | // source Expression. 101 | type compiledProgram struct { 102 | Program cel.Program 103 | Source *validate.Rule 104 | Path []*validate.FieldPathElement 105 | Value protoreflect.Value 106 | Descriptor protoreflect.FieldDescriptor 107 | } 108 | 109 | //nolint:nilnil // non-existence of violations is intentional 110 | func (expr compiledProgram) eval(bindings *variable, cfg *validationConfig) (*Violation, error) { 111 | now := globalNowPool.Get(cfg.nowFn) 112 | defer globalNowPool.Put(now) 113 | bindings.Next = now 114 | 115 | value, _, err := expr.Program.Eval(bindings) 116 | if err != nil { 117 | return nil, &RuntimeError{cause: fmt.Errorf( 118 | "error evaluating %s: %w", expr.Source.GetId(), err)} 119 | } 120 | switch val := value.Value().(type) { 121 | case string: 122 | if val == "" { 123 | return nil, nil 124 | } 125 | return &Violation{ 126 | Proto: &validate.Violation{ 127 | Rule: expr.rulePath(), 128 | RuleId: proto.String(expr.Source.GetId()), 129 | Message: proto.String(val), 130 | }, 131 | RuleValue: expr.Value, 132 | RuleDescriptor: expr.Descriptor, 133 | }, nil 134 | case bool: 135 | if val { 136 | return nil, nil 137 | } 138 | return &Violation{ 139 | Proto: &validate.Violation{ 140 | Rule: expr.rulePath(), 141 | RuleId: proto.String(expr.Source.GetId()), 142 | Message: proto.String(expr.Source.GetMessage()), 143 | }, 144 | RuleValue: expr.Value, 145 | RuleDescriptor: expr.Descriptor, 146 | }, nil 147 | default: 148 | return nil, &RuntimeError{cause: fmt.Errorf( 149 | "resolved to an unexpected type %T", val)} 150 | } 151 | } 152 | 153 | func (expr compiledProgram) rulePath() *validate.FieldPath { 154 | if len(expr.Path) > 0 { 155 | return &validate.FieldPath{Elements: expr.Path} 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /proto/tests/example/v1/compile.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package tests.example.v1; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | message MismatchRules { 22 | bool no_rule = 1; 23 | string string_field_bool_rule = 2 [(buf.validate.field).bool.const = true]; 24 | } 25 | 26 | message MixedValidInvalidRules { 27 | string string_field_bool_rule = 1 [(buf.validate.field).bool.const = true]; 28 | string valid_string_rule = 2 [(buf.validate.field).string.const = "foo"]; 29 | } 30 | -------------------------------------------------------------------------------- /proto/tests/example/v1/example.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package tests.example.v1; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | message Person { 22 | uint64 id = 1 [(buf.validate.field).uint64.gt = 999]; 23 | 24 | string email = 2 [(buf.validate.field).string.email = true]; 25 | 26 | string name = 3 [(buf.validate.field).string = { 27 | pattern: "^[[:alpha:]]+( [[:alpha:]]+)*$" 28 | max_bytes: 256 29 | }]; 30 | 31 | Coordinates home = 4; 32 | } 33 | 34 | message Coordinates { 35 | double lat = 1 [(buf.validate.field).double = { 36 | gte: -90 37 | lte: 90 38 | }]; 39 | double lng = 2 [(buf.validate.field).double = { 40 | gte: -180 41 | lte: 180 42 | }]; 43 | } 44 | -------------------------------------------------------------------------------- /proto/tests/example/v1/filter.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package tests.example.v1; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | message InvalidRules { 22 | option (buf.validate.message).cel = { 23 | id: "message_rule" 24 | message: "this message rule is invalid" 25 | expression: "this.invalid" 26 | }; 27 | 28 | int32 field = 1 [(buf.validate.field).cel = { 29 | id: "field_rule" 30 | message: "this field rule is invalid" 31 | expression: "this.invalid" 32 | }]; 33 | } 34 | 35 | message AllRuleTypes { 36 | option (buf.validate.message).cel = { 37 | id: "message_rule" 38 | message: "this message rule always fails" 39 | expression: "false" 40 | }; 41 | 42 | int32 field = 1 [(buf.validate.field).cel = { 43 | id: "field_rule" 44 | message: "this field rule always fails" 45 | expression: "false" 46 | }]; 47 | 48 | oneof required_oneof { 49 | option (buf.validate.oneof).required = true; 50 | string oneof_field = 2; 51 | } 52 | } 53 | 54 | message NestedRules { 55 | AllRuleTypes field = 1 [(buf.validate.field).cel = { 56 | id: "parent_field_rule" 57 | message: "this field rule always fails" 58 | expression: "false" 59 | }]; 60 | 61 | string field2 = 2 [(buf.validate.field).cel = { 62 | id: "parent_field_2_rule" 63 | message: "this field rule always fails" 64 | expression: "false" 65 | }]; 66 | 67 | repeated AllRuleTypes repeated_field = 3; 68 | 69 | map map_field = 4; 70 | 71 | oneof required_oneof { 72 | option (buf.validate.oneof).required = true; 73 | string oneof_field = 5; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /proto/tests/example/v1/predefined.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto2"; 16 | 17 | package tests.example.v1; 18 | 19 | import "buf/validate/validate.proto"; 20 | 21 | // https://github.com/bufbuild/protovalidate-go/issues/148 22 | message Issue148 { 23 | optional int32 test = 1 [ 24 | (buf.validate.field).int32.(abs_not_in) = 1, 25 | (buf.validate.field).int32.(abs_not_in) = -2 26 | ]; 27 | } 28 | 29 | // https://github.com/bufbuild/protovalidate-go/issues/187 30 | message Issue187 { 31 | optional bool false_field = 1 [(buf.validate.field).bool.(this_equals_rule) = false]; 32 | optional bool true_field = 2 [(buf.validate.field).bool.(this_equals_rule) = true]; 33 | } 34 | 35 | extend buf.validate.Int32Rules { 36 | repeated int32 abs_not_in = 1800 [(buf.validate.predefined).cel = { 37 | id: "int32.abs_not_in" 38 | expression: "this in rule || this in rule.map(n, -n)" 39 | message: "value must not be in absolute value of list" 40 | }]; 41 | } 42 | 43 | extend buf.validate.BoolRules { 44 | optional bool this_equals_rule = 1800 [(buf.validate.predefined).cel = { 45 | id: "bool.this_equals_rule" 46 | expression: "this == rule ? '' : 'this = %s, rule = %s'.format([string(this), string(rule)])" 47 | }]; 48 | } 49 | -------------------------------------------------------------------------------- /proto/tests/example/v1/validations.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Buf Technologies, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package tests.example.v1; 18 | 19 | import "buf/validate/validate.proto"; 20 | import "google/protobuf/any.proto"; 21 | import "google/protobuf/api.proto"; 22 | import "google/protobuf/field_mask.proto"; 23 | import "google/protobuf/timestamp.proto"; 24 | 25 | message HasMsgExprs { 26 | option (buf.validate.message).cel = { 27 | id: "x_lt_y" 28 | message: "x must be less than y" 29 | expression: "this.x < this.y" 30 | }; 31 | 32 | option (buf.validate.message).cel = { 33 | id: "y_gt_42" 34 | expression: 35 | "this.y > 42 ? ''" 36 | ": 'y must be greater than 42'" 37 | }; 38 | 39 | int32 x = 1 [ 40 | (buf.validate.field).cel = { 41 | id: "x_even" 42 | message: "x must be even" 43 | expression: "this % 2 == 0" 44 | }, 45 | (buf.validate.field).cel = { 46 | id: "x_coprime_3" 47 | expression: 48 | "this % 3 != 0 ? ''" 49 | ": 'x must not be divisible by 3'" 50 | } 51 | ]; 52 | int32 y = 2; 53 | } 54 | 55 | message SelfRecursive { 56 | option (buf.validate.message).cel = { 57 | id: "unique_turtles" 58 | message: "adjacent turtles must be unique" 59 | expression: "this.x != this.turtle.x" 60 | }; 61 | 62 | int32 x = 1; 63 | SelfRecursive turtle = 2 [(buf.validate.field).cel = { 64 | id: "non_zero_baby_turtle" 65 | message: "embedded turtle's x value must not be zero" 66 | expression: "this.x > 0" 67 | }]; 68 | } 69 | 70 | message LoopRecursiveA { 71 | LoopRecursiveB b = 1; 72 | } 73 | 74 | message LoopRecursiveB { 75 | LoopRecursiveA a = 1; 76 | } 77 | 78 | message MsgHasOneof { 79 | option (buf.validate.message).cel = { 80 | id: "test x" 81 | expression: 82 | "this.x == '' ? '' : \n" 83 | "!this.x.startsWith('foo') ? 'does not have prefix `foo`' : ''" 84 | }; 85 | 86 | option (buf.validate.message).cel = { 87 | id: "text y" 88 | expression: "this.y >= 0" 89 | }; 90 | 91 | oneof o { 92 | option (buf.validate.oneof).required = true; 93 | string x = 1 [(buf.validate.field).string.prefix = "foo"]; 94 | int32 y = 2 [(buf.validate.field).int32.gt = 0]; 95 | HasMsgExprs msg = 3; 96 | } 97 | } 98 | 99 | message MsgHasRepeated { 100 | repeated float x = 1 [(buf.validate.field).repeated = { 101 | max_items: 3 102 | min_items: 1 103 | items: { 104 | cel: { 105 | expression: "true" 106 | message: "intentional false" 107 | } 108 | float: {gt: 0} 109 | } 110 | unique: true 111 | }]; 112 | repeated string y = 2 [(buf.validate.field).repeated.unique = true]; 113 | repeated HasMsgExprs z = 3 [(buf.validate.field).repeated = {max_items: 2}]; 114 | } 115 | 116 | message MsgHasMap { 117 | map int32map = 1 [(buf.validate.field).map = { 118 | min_pairs: 3 119 | keys: { 120 | int32: {gt: 0} 121 | } 122 | values: { 123 | int32: {lt: 0} 124 | } 125 | }]; 126 | map string_map = 2 [(buf.validate.field).map = {max_pairs: 1}]; 127 | map message_map = 3 [(buf.validate.field).map = {min_pairs: 2}]; 128 | } 129 | 130 | message TransitiveFieldRule { 131 | google.protobuf.FieldMask mask = 1 [(buf.validate.field).cel = { 132 | id: "mask.paths" 133 | message: "mask.paths must not be empty" 134 | expression: "has(this.paths)" 135 | }]; 136 | } 137 | 138 | message MultipleStepsTransitiveFieldRules { 139 | google.protobuf.Api api = 1 [(buf.validate.field).cel = { 140 | id: "api.source_context.file_name" 141 | message: "api's source context file name must not be empty" 142 | expression: "has(this.source_context.file_name)" 143 | }]; 144 | } 145 | 146 | message Simple { 147 | string s = 1; 148 | } 149 | 150 | message FieldOfTypeAny { 151 | google.protobuf.Any any = 1 [(buf.validate.field).cel = { 152 | id: "any_type" 153 | message: "this should never fail" 154 | expression: "this == this" 155 | }]; 156 | } 157 | 158 | // https://github.com/bufbuild/protovalidate/issues/92 159 | message CelMapOnARepeated { 160 | repeated Value values = 1 [(buf.validate.field).cel = { 161 | id: "env.vars.unique" 162 | expression: "this.map(v, v.name).unique() ? '' : 'value names must be unique'" 163 | }]; 164 | 165 | message Value { 166 | string name = 1; 167 | } 168 | } 169 | 170 | message RepeatedItemCel { 171 | repeated string paths = 1 [(buf.validate.field).repeated.items.cel = { 172 | id: "paths.no_space" 173 | expression: "!this.startsWith(' ')" 174 | }]; 175 | } 176 | 177 | // https://github.com/bufbuild/protovalidate-go/issues/141 178 | 179 | message OneTwo { 180 | F1 field1 = 1; 181 | F2 field2 = 2; 182 | } 183 | 184 | message TwoOne { 185 | F2 field2 = 1; 186 | F1 field1 = 2; 187 | } 188 | 189 | message F1 { 190 | string need_this = 1; 191 | FieldWithIssue field = 2; 192 | } 193 | 194 | message F2 { 195 | FieldWithIssue field = 1; 196 | } 197 | 198 | message FieldWithIssue { 199 | F1 f1 = 1; 200 | string name = 2 [(buf.validate.field).string.min_len = 1]; 201 | } 202 | 203 | message Issue211 { 204 | google.protobuf.Timestamp value = 1 [(buf.validate.field).timestamp.gt_now = true]; 205 | } 206 | -------------------------------------------------------------------------------- /repeated.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | ) 22 | 23 | //nolint:gochecknoglobals 24 | var ( 25 | repeatedRuleDescriptor = (&validate.FieldRules{}).ProtoReflect().Descriptor().Fields().ByName("repeated") 26 | repeatedItemsRuleDescriptor = (&validate.RepeatedRules{}).ProtoReflect().Descriptor().Fields().ByName("items") 27 | repeatedItemsRulePath = &validate.FieldPath{ 28 | Elements: []*validate.FieldPathElement{ 29 | fieldPathElement(repeatedRuleDescriptor), 30 | fieldPathElement(repeatedItemsRuleDescriptor), 31 | }, 32 | } 33 | ) 34 | 35 | // listItems performs validation on the elements of a repeated field. 36 | type listItems struct { 37 | base 38 | 39 | // ItemRules are checked on every item of the list 40 | ItemRules value 41 | } 42 | 43 | func newListItems(valEval *value) listItems { 44 | return listItems{ 45 | base: newBase(valEval), 46 | ItemRules: value{NestedRule: repeatedItemsRulePath}, 47 | } 48 | } 49 | 50 | func (r listItems) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 51 | list := val.List() 52 | var ok bool 53 | var err error 54 | for i := range list.Len() { 55 | itemErr := r.ItemRules.EvaluateField(msg, list.Get(i), cfg, true) 56 | if itemErr != nil { 57 | updateViolationPaths(itemErr, &validate.FieldPathElement{ 58 | FieldNumber: proto.Int32(r.FieldPathElement.GetFieldNumber()), 59 | FieldType: r.base.FieldPathElement.GetFieldType().Enum(), 60 | FieldName: proto.String(r.FieldPathElement.GetFieldName()), 61 | Subscript: &validate.FieldPathElement_Index{Index: uint64(i)}, //nolint:gosec // indices are guaranteed to be non-negative 62 | }, r.RulePrefix.GetElements()) 63 | } 64 | if ok, err = mergeViolations(err, itemErr, cfg); !ok { 65 | return err 66 | } 67 | } 68 | return err 69 | } 70 | 71 | func (r listItems) Tautology() bool { 72 | return r.ItemRules.Tautology() 73 | } 74 | 75 | var _ evaluator = listItems{} 76 | -------------------------------------------------------------------------------- /resolve.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/reflect/protoreflect" 21 | "google.golang.org/protobuf/reflect/protoregistry" 22 | ) 23 | 24 | //nolint:gochecknoglobals // static data, only want single instance 25 | var resolver = newExtensionResolver() 26 | 27 | // ResolveMessageRules returns the ResolveMessageRules option set for the MessageDescriptor. 28 | func ResolveMessageRules(desc protoreflect.MessageDescriptor) (*validate.MessageRules, error) { 29 | return resolve[*validate.MessageRules](desc.Options(), validate.E_Message) 30 | } 31 | 32 | // ResolveOneofRules returns the ResolveOneofRules option set for the OneofDescriptor. 33 | func ResolveOneofRules(desc protoreflect.OneofDescriptor) (*validate.OneofRules, error) { 34 | return resolve[*validate.OneofRules](desc.Options(), validate.E_Oneof) 35 | } 36 | 37 | // ResolveFieldRules returns the ResolveFieldRules option set for the FieldDescriptor. 38 | func ResolveFieldRules(desc protoreflect.FieldDescriptor) (*validate.FieldRules, error) { 39 | return resolve[*validate.FieldRules](desc.Options(), validate.E_Field) 40 | } 41 | 42 | // ResolvePredefinedRules returns the ResolvePredefinedRules option set for the 43 | // FieldDescriptor. Note that this value is only meaningful if it is set on a 44 | // field or extension of a field rule message. This method is provided for 45 | // convenience. 46 | func ResolvePredefinedRules(desc protoreflect.FieldDescriptor) (*validate.PredefinedRules, error) { 47 | return resolve[*validate.PredefinedRules](desc.Options(), validate.E_Predefined) 48 | } 49 | 50 | // resolve resolves extensions without using [proto.GetExtension], in case the 51 | // underlying type of the extension is not the concrete type expected by the 52 | // library. In some cases, particularly when using a dynamic descriptor set, the 53 | // underlying extension value's type will be a dynamicpb.Message. In some cases, 54 | // the extension may not be resolved at all. This function handles reparsing the 55 | // fields as needed to get it into the right concrete message. Resolve does not 56 | // modify the input protobuf message, so it can be used concurrently. 57 | func resolve[C proto.Message]( 58 | options proto.Message, 59 | extensionType protoreflect.ExtensionType, 60 | ) (typedMessage C, err error) { 61 | var nilMessage C 62 | message, err := resolver.resolve(options, extensionType) 63 | if err != nil { 64 | return nilMessage, err 65 | } 66 | if message == nil { 67 | return nilMessage, nil 68 | } else if typedMessage, ok := message.(C); ok { 69 | return typedMessage, nil 70 | } 71 | typedMessage, _ = typedMessage.ProtoReflect().New().Interface().(C) 72 | b, err := proto.Marshal(message) 73 | if err != nil { 74 | return nilMessage, err 75 | } 76 | err = proto.Unmarshal(b, typedMessage) 77 | if err != nil { 78 | return nilMessage, err 79 | } 80 | return typedMessage, nil 81 | } 82 | 83 | // extensionResolver implements most of the logic of resolving protovalidate 84 | // extensions. 85 | type extensionResolver struct { 86 | // types is a types that just contains the protovalidate extensions. 87 | types *protoregistry.Types 88 | } 89 | 90 | // newExtensionResolver creates a new extension resolver. This is only called at 91 | // init and will panic if it fails. 92 | func newExtensionResolver() extensionResolver { 93 | resolver := extensionResolver{ 94 | types: &protoregistry.Types{}, 95 | } 96 | resolver.register(validate.E_Field) 97 | resolver.register(validate.E_Message) 98 | resolver.register(validate.E_Oneof) 99 | resolver.register(validate.E_Predefined) 100 | return resolver 101 | } 102 | 103 | // register registers an extension into the resolver's registry. This is only 104 | // called at init and will panic if it fails. 105 | func (resolver extensionResolver) register(extension protoreflect.ExtensionType) { 106 | if err := resolver.types.RegisterExtension(extension); err != nil { 107 | //nolint:forbidigo // this needs to be a fatal at init 108 | panic(err) 109 | } 110 | } 111 | 112 | // resolve handles the majority of extension resolution logic. This will return 113 | // a proto.Message for the given extension if the message has the tag number of 114 | // the provided extension. If there was no such tag number present in the known 115 | // or unknown fields, this method will return nil. Note that the returned 116 | // message may be dynamicpb.Message or another type, and thus may need to still 117 | // be reparsed if needed. 118 | func (resolver extensionResolver) resolve( 119 | options proto.Message, 120 | extensionType protoreflect.ExtensionType, 121 | ) (msg proto.Message, err error) { 122 | msg = resolver.getExtension(options, extensionType) 123 | if msg == nil { 124 | if unknown := options.ProtoReflect().GetUnknown(); len(unknown) > 0 { 125 | reparsedOptions := options.ProtoReflect().Type().New().Interface() 126 | if err = (proto.UnmarshalOptions{ 127 | Resolver: resolver.types, 128 | }).Unmarshal(unknown, reparsedOptions); err == nil { 129 | msg = resolver.getExtension(reparsedOptions, extensionType) 130 | } 131 | } 132 | } 133 | if err != nil { 134 | return nil, err 135 | } 136 | return msg, nil 137 | } 138 | 139 | // getExtension gets the extension extensionType on message, or if it is not 140 | // found, nil. Unlike proto.GetExtension, this method will not panic if the 141 | // runtime type of the extension is unexpected and returns nil if the extension 142 | // is not present. 143 | func (resolver extensionResolver) getExtension( 144 | message proto.Message, 145 | extensionType protoreflect.ExtensionType, 146 | ) proto.Message { 147 | reflect := message.ProtoReflect() 148 | if reflect.Has(extensionType.TypeDescriptor()) { 149 | extension, _ := reflect.Get(extensionType.TypeDescriptor()).Interface().(protoreflect.Message) 150 | return extension.Interface() 151 | } 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /resolve_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 21 | "github.com/stretchr/testify/require" 22 | "google.golang.org/protobuf/encoding/protowire" 23 | "google.golang.org/protobuf/proto" 24 | "google.golang.org/protobuf/reflect/protoreflect" 25 | "google.golang.org/protobuf/types/descriptorpb" 26 | ) 27 | 28 | func TestResolve(t *testing.T) { 29 | t.Parallel() 30 | 31 | expectedRules := &validate.FieldRules{ 32 | Cel: []*validate.Rule{ 33 | {Message: proto.String("test")}, 34 | }, 35 | } 36 | expectedRulesBytes, err := proto.Marshal(expectedRules) 37 | require.NoError(t, err) 38 | 39 | tests := []struct { 40 | name string 41 | builder func() proto.Message 42 | }{ 43 | { 44 | name: "Normal", 45 | builder: func() proto.Message { 46 | options := &descriptorpb.FieldOptions{} 47 | proto.SetExtension(options, validate.E_Field, expectedRules) 48 | return options 49 | }, 50 | }, 51 | { 52 | name: "Dynamic", 53 | builder: func() proto.Message { 54 | var unknownBytes []byte 55 | unknownBytes = protowire.AppendTag( 56 | unknownBytes, 57 | validate.E_Field.TypeDescriptor().Number(), 58 | protowire.BytesType, 59 | ) 60 | unknownBytes = protowire.AppendBytes( 61 | unknownBytes, 62 | expectedRulesBytes, 63 | ) 64 | options := &descriptorpb.FieldOptions{} 65 | options.ProtoReflect().SetUnknown(protoreflect.RawFields(unknownBytes)) 66 | return options 67 | }, 68 | }, 69 | { 70 | name: "Unknown", 71 | builder: func() proto.Message { 72 | var unknownBytes []byte 73 | unknownBytes = protowire.AppendTag( 74 | unknownBytes, 75 | validate.E_Field.TypeDescriptor().Number(), 76 | protowire.BytesType, 77 | ) 78 | unknownBytes = protowire.AppendBytes( 79 | unknownBytes, 80 | expectedRulesBytes, 81 | ) 82 | options := &descriptorpb.FieldOptions{} 83 | options.ProtoReflect().SetUnknown(protoreflect.RawFields(unknownBytes)) 84 | return options 85 | }, 86 | }, 87 | } 88 | 89 | for _, tc := range tests { 90 | test := tc 91 | t.Run(test.name, func(t *testing.T) { 92 | t.Parallel() 93 | 94 | pb := test.builder() 95 | extension, err := resolve[*validate.FieldRules](pb, validate.E_Field) 96 | require.NoError(t, err) 97 | require.NotNil(t, extension) 98 | require.Equal(t, "test", extension.GetCel()[0].GetMessage()) 99 | }) 100 | } 101 | } 102 | 103 | func TestResolveNone(t *testing.T) { 104 | t.Parallel() 105 | extension, err := resolve[*validate.FieldRules]( 106 | &descriptorpb.FieldOptions{}, 107 | validate.E_Field, 108 | ) 109 | require.NoError(t, err) 110 | require.Nil(t, extension) 111 | } 112 | -------------------------------------------------------------------------------- /runtime_error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import "strings" 18 | 19 | // A RuntimeError is returned if a valid CEL expression evaluation is terminated. 20 | // The two built-in reasons are 'no_matching_overload' when a CEL function has 21 | // no overload for the types of the arguments or 'no_such_field' when a map or 22 | // message does not contain the desired field. 23 | type RuntimeError struct { 24 | cause error 25 | } 26 | 27 | func (err *RuntimeError) Error() string { 28 | if err == nil { 29 | return "" 30 | } 31 | var builder strings.Builder 32 | _, _ = builder.WriteString("runtime error") 33 | if err.cause != nil { 34 | _, _ = builder.WriteString(": ") 35 | _, _ = builder.WriteString(err.cause.Error()) 36 | } 37 | return builder.String() 38 | } 39 | 40 | func (err *RuntimeError) Unwrap() error { 41 | if err == nil { 42 | return nil 43 | } 44 | return err.cause 45 | } 46 | -------------------------------------------------------------------------------- /validation_error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 22 | ) 23 | 24 | // A ValidationError is returned if one or more rule violations were 25 | // detected. 26 | type ValidationError struct { 27 | Violations []*Violation 28 | } 29 | 30 | func (err *ValidationError) Error() string { 31 | bldr := &strings.Builder{} 32 | bldr.WriteString("validation error:") 33 | for _, violation := range err.Violations { 34 | bldr.WriteString("\n - ") 35 | if fieldPath := FieldPathString(violation.Proto.GetField()); fieldPath != "" { 36 | bldr.WriteString(fieldPath) 37 | bldr.WriteString(": ") 38 | } 39 | _, _ = fmt.Fprintf(bldr, "%s [%s]", 40 | violation.Proto.GetMessage(), 41 | violation.Proto.GetRuleId()) 42 | } 43 | return bldr.String() 44 | } 45 | 46 | // ToProto converts this error into its proto.Message form. 47 | func (err *ValidationError) ToProto() *validate.Violations { 48 | violations := &validate.Violations{ 49 | Violations: make([]*validate.Violation, len(err.Violations)), 50 | } 51 | for i, violation := range err.Violations { 52 | violations.Violations[i] = violation.Proto 53 | } 54 | return violations 55 | } 56 | -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "fmt" 19 | "sync" 20 | 21 | pvcel "buf.build/go/protovalidate/cel" 22 | "github.com/google/cel-go/cel" 23 | "google.golang.org/protobuf/proto" 24 | "google.golang.org/protobuf/reflect/protoreflect" 25 | "google.golang.org/protobuf/reflect/protoregistry" 26 | "google.golang.org/protobuf/types/known/timestamppb" 27 | ) 28 | 29 | var ( 30 | getGlobalValidator = sync.OnceValues(func() (Validator, error) { return New() }) 31 | 32 | // GlobalValidator provides access to the global Validator instance that is 33 | // used by the [Validate] function. This is intended to be used by libraries 34 | // that use protovalidate. This Validator can be used as a default when the 35 | // user does not specify a Validator instance to use. 36 | // 37 | // Using the global Validator instance (either through [Validator] or via 38 | // GlobalValidator) will result in lower memory usage than using multiple 39 | // Validator instances, because each Validator instance has its own caches. 40 | GlobalValidator Validator = globalValidator{} 41 | ) 42 | 43 | // Validator performs validation on any proto.Message values. The Validator is 44 | // safe for concurrent use. 45 | type Validator interface { 46 | // Validate checks that message satisfies its rules. Rules are 47 | // defined within the Protobuf file as options from the buf.validate 48 | // package. An error is returned if the rules are violated 49 | // (ValidationError), the evaluation logic for the message cannot be built 50 | // (CompilationError), or there is a type error when attempting to evaluate 51 | // a CEL expression associated with the message (RuntimeError). 52 | Validate(msg proto.Message, options ...ValidationOption) error 53 | } 54 | 55 | // New creates a Validator with the given options. An error may occur in setting 56 | // up the CEL execution environment if the configuration is invalid. See the 57 | // individual ValidatorOption for how they impact the fallibility of New. 58 | func New(options ...ValidatorOption) (Validator, error) { 59 | cfg := config{ 60 | extensionTypeResolver: protoregistry.GlobalTypes, 61 | nowFn: timestamppb.Now, 62 | } 63 | for _, opt := range options { 64 | opt.applyToValidator(&cfg) 65 | } 66 | 67 | env, err := cel.NewEnv(cel.Lib(pvcel.NewLibrary())) 68 | if err != nil { 69 | return nil, fmt.Errorf( 70 | "failed to construct CEL environment: %w", err) 71 | } 72 | 73 | bldr := newBuilder( 74 | env, 75 | cfg.disableLazy, 76 | cfg.extensionTypeResolver, 77 | cfg.allowUnknownFields, 78 | cfg.desc..., 79 | ) 80 | 81 | return &validator{ 82 | failFast: cfg.failFast, 83 | builder: bldr, 84 | nowFn: cfg.nowFn, 85 | }, nil 86 | } 87 | 88 | type validator struct { 89 | builder *builder 90 | failFast bool 91 | nowFn func() *timestamppb.Timestamp 92 | } 93 | 94 | func (v *validator) Validate( 95 | msg proto.Message, 96 | options ...ValidationOption, 97 | ) error { 98 | if msg == nil { 99 | return nil 100 | } 101 | cfg := validationConfig{ 102 | failFast: v.failFast, 103 | filter: nopFilter{}, 104 | nowFn: v.nowFn, 105 | } 106 | for _, opt := range options { 107 | opt.applyToValidation(&cfg) 108 | } 109 | refl := msg.ProtoReflect() 110 | eval := v.builder.Load(refl.Descriptor()) 111 | err := eval.EvaluateMessage(refl, &cfg) 112 | finalizeViolationPaths(err) 113 | return err 114 | } 115 | 116 | // Validate uses a global instance of Validator constructed with no ValidatorOptions and 117 | // calls its Validate function. For the vast majority of validation cases, using this global 118 | // function is safe and acceptable. If you need to provide i.e. a custom 119 | // ExtensionTypeResolver, you'll need to construct a Validator. 120 | func Validate(msg proto.Message, options ...ValidationOption) error { 121 | globalValidator, err := getGlobalValidator() 122 | if err != nil { 123 | return err 124 | } 125 | return globalValidator.Validate(msg, options...) 126 | } 127 | 128 | type config struct { 129 | failFast bool 130 | disableLazy bool 131 | desc []protoreflect.MessageDescriptor 132 | extensionTypeResolver protoregistry.ExtensionTypeResolver 133 | allowUnknownFields bool 134 | nowFn func() *timestamppb.Timestamp 135 | } 136 | 137 | type validationConfig struct { 138 | failFast bool 139 | filter Filter 140 | nowFn func() *timestamppb.Timestamp 141 | } 142 | 143 | type globalValidator struct{} 144 | 145 | func (globalValidator) Validate(msg proto.Message, options ...ValidationOption) error { 146 | return Validate(msg, options...) 147 | } 148 | -------------------------------------------------------------------------------- /validator_bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | pb "buf.build/go/protovalidate/internal/gen/tests/example/v1" 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func BenchmarkValidator(b *testing.B) { 26 | successMsg := &pb.HasMsgExprs{X: 2, Y: 43} 27 | failureMsg := &pb.HasMsgExprs{X: 9, Y: 2} 28 | 29 | b.Run("ColdStart", func(b *testing.B) { 30 | b.ReportAllocs() 31 | b.RunParallel(func(p *testing.PB) { 32 | for p.Next() { 33 | val, err := New() 34 | require.NoError(b, err) 35 | err = val.Validate(successMsg) 36 | require.NoError(b, err) 37 | } 38 | }) 39 | }) 40 | 41 | b.Run("Lazy/Valid", func(b *testing.B) { 42 | b.ReportAllocs() 43 | val, err := New() 44 | require.NoError(b, err) 45 | b.ResetTimer() 46 | b.RunParallel(func(p *testing.PB) { 47 | for p.Next() { 48 | err := val.Validate(successMsg) 49 | require.NoError(b, err) 50 | } 51 | }) 52 | }) 53 | 54 | b.Run("Lazy/Invalid", func(b *testing.B) { 55 | b.ReportAllocs() 56 | val, err := New() 57 | require.NoError(b, err) 58 | b.ResetTimer() 59 | b.RunParallel(func(p *testing.PB) { 60 | for p.Next() { 61 | err := val.Validate(failureMsg) 62 | assert.Error(b, err) 63 | } 64 | }) 65 | }) 66 | 67 | b.Run("Lazy/FailFast", func(b *testing.B) { 68 | b.ReportAllocs() 69 | val, err := New(WithFailFast()) 70 | require.NoError(b, err) 71 | b.ResetTimer() 72 | b.RunParallel(func(p *testing.PB) { 73 | for p.Next() { 74 | err := val.Validate(failureMsg) 75 | assert.Error(b, err) 76 | } 77 | }) 78 | }) 79 | 80 | b.Run("PreWarmed/Valid", func(b *testing.B) { 81 | b.ReportAllocs() 82 | val, err := New( 83 | WithMessages(successMsg), 84 | WithDisableLazy(), 85 | ) 86 | require.NoError(b, err) 87 | b.ResetTimer() 88 | b.RunParallel(func(p *testing.PB) { 89 | for p.Next() { 90 | err := val.Validate(successMsg) 91 | require.NoError(b, err) 92 | } 93 | }) 94 | }) 95 | 96 | b.Run("PreWarmed/Invalid", func(b *testing.B) { 97 | b.ReportAllocs() 98 | val, err := New( 99 | WithMessages(failureMsg), 100 | WithDisableLazy(), 101 | ) 102 | require.NoError(b, err) 103 | b.ResetTimer() 104 | b.RunParallel(func(p *testing.PB) { 105 | for p.Next() { 106 | err := val.Validate(failureMsg) 107 | assert.Error(b, err) 108 | } 109 | }) 110 | }) 111 | 112 | b.Run("PreWarmed/FailFast", func(b *testing.B) { 113 | b.ReportAllocs() 114 | val, err := New( 115 | WithFailFast(), 116 | WithMessages(failureMsg), 117 | WithDisableLazy(), 118 | ) 119 | require.NoError(b, err) 120 | b.ResetTimer() 121 | b.RunParallel(func(p *testing.PB) { 122 | for p.Next() { 123 | err := val.Validate(failureMsg) 124 | assert.Error(b, err) 125 | } 126 | }) 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /validator_example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "log" 21 | "os" 22 | "text/template" 23 | 24 | pb "buf.build/go/protovalidate/internal/gen/tests/example/v1" 25 | "google.golang.org/protobuf/reflect/protoregistry" 26 | ) 27 | 28 | func Example() { 29 | person := &pb.Person{ 30 | Id: 1234, 31 | Email: "protovalidate@buf.build", 32 | Name: "Buf Build", 33 | Home: &pb.Coordinates{ 34 | Lat: 27.380583333333334, 35 | Lng: 33.631838888888886, 36 | }, 37 | } 38 | 39 | err := Validate(person) 40 | fmt.Println("valid:", err) 41 | 42 | person.Email = "not an email" 43 | err = Validate(person) 44 | fmt.Println("invalid:", err) 45 | 46 | // output: 47 | // valid: 48 | // invalid: validation error: 49 | // - email: value must be a valid email address [string.email] 50 | } 51 | 52 | func ExampleWithFailFast() { 53 | loc := &pb.Coordinates{Lat: 999.999, Lng: -999.999} 54 | 55 | validator, err := New() 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | err = validator.Validate(loc) 60 | fmt.Println("default:", err) 61 | 62 | validator, err = New(WithFailFast()) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | err = validator.Validate(loc) 67 | fmt.Println("fail fast:", err) 68 | 69 | // output: 70 | // default: validation error: 71 | // - lat: value must be greater than or equal to -90 and less than or equal to 90 [double.gte_lte] 72 | // - lng: value must be greater than or equal to -180 and less than or equal to 180 [double.gte_lte] 73 | // fail fast: validation error: 74 | // - lat: value must be greater than or equal to -90 and less than or equal to 90 [double.gte_lte] 75 | } 76 | 77 | func ExampleWithMessages() { 78 | validator, err := New( 79 | WithMessages(&pb.Person{}), 80 | ) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | 85 | person := &pb.Person{ 86 | Id: 1234, 87 | Email: "protovalidate@buf.build", 88 | Name: "Protocol Buffer", 89 | } 90 | err = validator.Validate(person) 91 | fmt.Println(err) 92 | 93 | // output: 94 | } 95 | 96 | func ExampleWithMessageDescriptors() { 97 | pbType, err := protoregistry.GlobalTypes.FindMessageByName("tests.example.v1.Person") 98 | if err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | validator, err := New( 103 | WithMessageDescriptors( 104 | pbType.Descriptor(), 105 | ), 106 | ) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | person := &pb.Person{ 112 | Id: 1234, 113 | Email: "protovalidate@buf.build", 114 | Name: "Protocol Buffer", 115 | } 116 | err = validator.Validate(person) 117 | fmt.Println(err) 118 | 119 | // output: 120 | } 121 | 122 | func ExampleWithDisableLazy() { 123 | person := &pb.Person{ 124 | Id: 1234, 125 | Email: "protovalidate@buf.build", 126 | Name: "Buf Build", 127 | Home: &pb.Coordinates{ 128 | Lat: 27.380583333333334, 129 | Lng: 33.631838888888886, 130 | }, 131 | } 132 | 133 | validator, err := New( 134 | WithMessages(&pb.Coordinates{}), 135 | WithDisableLazy(), 136 | ) 137 | if err != nil { 138 | log.Fatal(err) 139 | } 140 | 141 | err = validator.Validate(person.GetHome()) 142 | fmt.Println("person.Home:", err) 143 | err = validator.Validate(person) 144 | fmt.Println("person:", err) 145 | 146 | // output: 147 | // person.Home: 148 | // person: compilation error: no evaluator available for tests.example.v1.Person 149 | } 150 | 151 | func ExampleValidationError() { 152 | validator, err := New() 153 | if err != nil { 154 | log.Fatal(err) 155 | } 156 | 157 | loc := &pb.Coordinates{Lat: 999.999} 158 | err = validator.Validate(loc) 159 | var valErr *ValidationError 160 | if ok := errors.As(err, &valErr); ok { 161 | violation := valErr.Violations[0] 162 | fmt.Println(violation.Proto.GetField().GetElements()[0].GetFieldName(), violation.Proto.GetRuleId()) 163 | fmt.Println(violation.RuleValue, violation.FieldValue) 164 | } 165 | 166 | // output: lat double.gte_lte 167 | // -90 999.999 168 | } 169 | 170 | func ExampleValidationError_localized() { 171 | validator, err := New() 172 | if err != nil { 173 | log.Fatal(err) 174 | } 175 | 176 | type ErrorInfo struct { 177 | FieldName string 178 | RuleValue any 179 | FieldValue any 180 | } 181 | 182 | var ruleMessages = map[string]string{ 183 | "string.email_empty": "{{.FieldName}}: メールアドレスは空であってはなりません。\n", 184 | "string.pattern": "{{.FieldName}}: 値はパターン「{{.RuleValue}}」一致する必要があります。\n", 185 | "uint64.gt": "{{.FieldName}}: 値は{{.RuleValue}}を超える必要があります。(価値:{{.FieldValue}})\n", 186 | } 187 | 188 | loc := &pb.Person{Id: 900} 189 | err = validator.Validate(loc) 190 | var valErr *ValidationError 191 | if ok := errors.As(err, &valErr); ok { 192 | for _, violation := range valErr.Violations { 193 | _ = template. 194 | Must(template.New("").Parse(ruleMessages[violation.Proto.GetRuleId()])). 195 | Execute(os.Stdout, ErrorInfo{ 196 | FieldName: violation.Proto.GetField().GetElements()[0].GetFieldName(), 197 | RuleValue: violation.RuleValue.Interface(), 198 | FieldValue: violation.FieldValue.Interface(), 199 | }) 200 | } 201 | } 202 | 203 | // output: 204 | // id: 値は999を超える必要があります。(価値:900) 205 | // email: メールアドレスは空であってはなりません。 206 | // name: 値はパターン「^[[:alpha:]]+( [[:alpha:]]+)*$」一致する必要があります。 207 | } 208 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/reflect/protoreflect" 20 | ) 21 | 22 | // value performs validation on any concrete value contained within a singular 23 | // field, repeated elements, or the keys/values of a map. 24 | type value struct { 25 | // Descriptor is the FieldDescriptor targeted by this evaluator 26 | Descriptor protoreflect.FieldDescriptor 27 | // Rules are the individual evaluators applied to a value 28 | Rules evaluators 29 | // NestedRules are rules applied to messages nested under a 30 | // value. 31 | NestedRules evaluators 32 | // Zero is the default or zero-value for this value's type 33 | Zero protoreflect.Value 34 | // NestedRule specifies the nested rule type the value is for. 35 | NestedRule *validate.FieldPath 36 | // IgnoreEmpty indicates that the Rules should not be applied if the 37 | // value is unset or the default (typically zero) value. This only applies to 38 | // repeated elements or map keys/values with an ignore_empty rule. 39 | IgnoreEmpty bool 40 | } 41 | 42 | func (v *value) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { 43 | return v.EvaluateField(msg, val, cfg, cfg.filter.ShouldValidate(msg, v.Descriptor)) 44 | } 45 | 46 | func (v *value) EvaluateField( 47 | msg protoreflect.Message, 48 | val protoreflect.Value, 49 | cfg *validationConfig, 50 | shouldValidate bool, 51 | ) error { 52 | var ( 53 | err error 54 | ok bool 55 | ) 56 | if shouldValidate { 57 | if v.IgnoreEmpty && val.Equal(v.Zero) { 58 | return nil 59 | } 60 | if ok, err = mergeViolations(err, v.Rules.Evaluate(msg, val, cfg), cfg); !ok { 61 | return err 62 | } 63 | } 64 | _, err = mergeViolations(err, v.NestedRules.Evaluate(msg, val, cfg), cfg) 65 | return err 66 | } 67 | 68 | func (v *value) Tautology() bool { 69 | return v.Rules.Tautology() && v.NestedRules.Tautology() 70 | } 71 | 72 | func (v *value) Append(eval evaluator) { 73 | if !eval.Tautology() { 74 | v.Rules = append(v.Rules, eval) 75 | } 76 | } 77 | 78 | func (v *value) AppendNested(eval evaluator) { 79 | if !eval.Tautology() { 80 | v.NestedRules = append(v.NestedRules, eval) 81 | } 82 | } 83 | 84 | var _ evaluator = (*value)(nil) 85 | -------------------------------------------------------------------------------- /variable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/google/cel-go/interpreter" 21 | "google.golang.org/protobuf/types/known/timestamppb" 22 | ) 23 | 24 | // variable implements interpreter.Activation, providing a lightweight named 25 | // variable to cel.Program executions. 26 | type variable struct { 27 | // Next is the parent activation 28 | Next interpreter.Activation 29 | // Name is the variable's name 30 | Name string 31 | // Val is the value for this variable 32 | Val any 33 | } 34 | 35 | func (v *variable) ResolveName(name string) (any, bool) { 36 | switch { 37 | case name == v.Name: 38 | return v.Val, true 39 | case v.Next != nil: 40 | return v.Next.ResolveName(name) 41 | default: 42 | return nil, false 43 | } 44 | } 45 | 46 | func (v *variable) Parent() interpreter.Activation { return nil } 47 | 48 | type variablePool sync.Pool 49 | 50 | func (p *variablePool) Put(v *variable) { 51 | (*sync.Pool)(p).Put(v) 52 | } 53 | 54 | func (p *variablePool) Get() *variable { 55 | v := (*sync.Pool)(p).Get().(*variable) //nolint:errcheck,forcetypeassert 56 | v.Next = nil 57 | return v 58 | } 59 | 60 | // now implements interpreter.Activation, providing a lazily produced timestamp 61 | // for accessing the variable `now` that's constant within an evaluation. 62 | type now struct { 63 | // TS is the already resolved timestamp. If unset, the field is populated with 64 | // the output of nowFn. 65 | TS *timestamppb.Timestamp 66 | nowFn func() *timestamppb.Timestamp 67 | } 68 | 69 | func (n *now) ResolveName(name string) (any, bool) { 70 | if name != "now" { 71 | return nil, false 72 | } else if n.TS == nil { 73 | n.TS = n.nowFn() 74 | } 75 | return n.TS, true 76 | } 77 | 78 | func (n *now) Parent() interpreter.Activation { return nil } 79 | 80 | type nowPool sync.Pool 81 | 82 | func (p *nowPool) Put(v *now) { 83 | (*sync.Pool)(p).Put(v) 84 | } 85 | 86 | func (p *nowPool) Get(nowFn func() *timestamppb.Timestamp) *now { 87 | n := (*sync.Pool)(p).Get().(*now) //nolint:errcheck,forcetypeassert 88 | n.nowFn = nowFn 89 | n.TS = nil 90 | return n 91 | } 92 | -------------------------------------------------------------------------------- /variable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestVariable(t *testing.T) { 24 | t.Parallel() 25 | 26 | v := variable{Name: "this", Val: "foo"} 27 | out, ok := v.ResolveName("this") 28 | assert.True(t, ok) 29 | assert.Equal(t, v.Val, out) 30 | _, ok = v.ResolveName("that") 31 | assert.False(t, ok) 32 | } 33 | -------------------------------------------------------------------------------- /violation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 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 protovalidate 16 | 17 | import ( 18 | "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" 19 | "google.golang.org/protobuf/reflect/protoreflect" 20 | ) 21 | 22 | // Violation represents a single instance where a validation rule was not met. 23 | // It provides information about the field that caused the violation, the 24 | // specific unfulfilled rule, and a human-readable error message. 25 | type Violation struct { 26 | // Proto contains the violation's proto.Message form. 27 | Proto *validate.Violation 28 | 29 | // FieldValue contains the value of the specific field that failed 30 | // validation. If there was no value, this will contain an invalid value. 31 | FieldValue protoreflect.Value 32 | 33 | // FieldDescriptor contains the field descriptor corresponding to the 34 | // field that failed validation. 35 | FieldDescriptor protoreflect.FieldDescriptor 36 | 37 | // RuleValue contains the value of the rule that specified the failed 38 | // rule. Not all rules have a value; only standard and 39 | // predefined rules have rule values. In violations caused by other 40 | // kinds of rules, like custom contraints, this will contain an 41 | // invalid value. 42 | RuleValue protoreflect.Value 43 | 44 | // RuleDescriptor contains the field descriptor corresponding to the 45 | // rule that failed validation. 46 | RuleDescriptor protoreflect.FieldDescriptor 47 | } 48 | --------------------------------------------------------------------------------