├── .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 |
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 | [][buf]
2 |
3 | # protovalidate-go
4 |
5 | [](https://github.com/bufbuild/protovalidate-go/actions/workflows/ci.yaml)
6 | [](https://github.com/bufbuild/protovalidate-go/actions/workflows/conformance.yaml)
7 | [](https://goreportcard.com/report/buf.build/go/protovalidate)
8 | [](https://pkg.go.dev/buf.build/go/protovalidate)
9 | [][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 | [][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 |
--------------------------------------------------------------------------------