evaluate(Value val, boolean failFast)
79 | throws ExecutionException {
80 | Object enumValue = val.value(Object.class);
81 | if (enumValue == null) {
82 | return RuleViolation.NO_VIOLATIONS;
83 | }
84 | if (!values.contains(enumValue)) {
85 | return Collections.singletonList(
86 | RuleViolation.newBuilder()
87 | .addAllRulePathElements(helper.getRulePrefixElements())
88 | .addAllRulePathElements(DEFINED_ONLY_RULE_PATH.getElementsList())
89 | .addFirstFieldPathElement(helper.getFieldPathElement())
90 | .setRuleId("enum.defined_only")
91 | .setMessage("value must be one of the defined enum values")
92 | .setFieldValue(new RuleViolation.FieldValue(val))
93 | .setRuleValue(new RuleViolation.FieldValue(true, DEFINED_ONLY_DESCRIPTOR)));
94 | }
95 | return RuleViolation.NO_VIOLATIONS;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ValidatorFactory.java:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2025 Buf Technologies, Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package build.buf.protovalidate;
16 |
17 | import build.buf.protovalidate.exceptions.CompilationException;
18 | import com.google.protobuf.Descriptors.Descriptor;
19 | import java.util.List;
20 | import org.jspecify.annotations.Nullable;
21 |
22 | /**
23 | * ValidatorFactory is used to create a validator.
24 | *
25 | * Validators can be created with an optional {@link Config} to customize behavior. They can also
26 | * be created with a list of seed descriptors to warmup the validator cache ahead of time as well as
27 | * an indicator to lazily-load any descriptors not provided into the cache.
28 | */
29 | public final class ValidatorFactory {
30 | // Prevent instantiation
31 | private ValidatorFactory() {}
32 |
33 | /** A builder class used for building a validator. */
34 | public static class ValidatorBuilder {
35 | /** The config object to use for instantiating a validator. */
36 | @Nullable private Config config;
37 |
38 | /**
39 | * Create a validator with the given config
40 | *
41 | * @param config The {@link Config} to configure the validator.
42 | * @return The builder instance
43 | */
44 | public ValidatorBuilder withConfig(Config config) {
45 | this.config = config;
46 | return this;
47 | }
48 |
49 | // Prevent instantiation
50 | private ValidatorBuilder() {}
51 |
52 | /**
53 | * Build a new validator
54 | *
55 | * @return A new {@link Validator} instance.
56 | */
57 | public Validator build() {
58 | Config cfg = this.config;
59 | if (cfg == null) {
60 | cfg = Config.newBuilder().build();
61 | }
62 | return new ValidatorImpl(cfg);
63 | }
64 |
65 | /**
66 | * Build the validator, warming up the cache with any provided descriptors.
67 | *
68 | * @param descriptors the list of descriptors to warm up the cache.
69 | * @param disableLazy whether to disable lazy loading of validation rules. When validation is
70 | * performed, a message's rules will be looked up in a cache. If they are not found, by
71 | * default they will be processed and lazily-loaded into the cache. Setting this to false
72 | * will not attempt to lazily-load descriptor information not found in the cache and
73 | * essentially makes the entire cache read-only, eliminating thread contention.
74 | * @return A new {@link Validator} instance.
75 | * @throws CompilationException If any of the given descriptors' validation rules fail
76 | * processing while warming up the cache.
77 | * @throws IllegalStateException If disableLazy is set to true and no descriptors are passed.
78 | */
79 | public Validator buildWithDescriptors(List descriptors, boolean disableLazy)
80 | throws CompilationException, IllegalStateException {
81 | if (disableLazy && (descriptors == null || descriptors.isEmpty())) {
82 | throw new IllegalStateException(
83 | "a list of descriptors is required when disableLazy is true");
84 | }
85 |
86 | Config cfg = this.config;
87 | if (cfg == null) {
88 | cfg = Config.newBuilder().build();
89 | }
90 | return new ValidatorImpl(cfg, descriptors, disableLazy);
91 | }
92 | }
93 |
94 | /**
95 | * Creates a new builder for a validator.
96 | *
97 | * @return A Validator builder
98 | */
99 | public static ValidatorBuilder newBuilder() {
100 | return new ValidatorBuilder();
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/test/resources/proto/validationtest/validationtest.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2025 Buf Technologies, Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | syntax = "proto3";
16 |
17 | package validationtest;
18 |
19 | import "buf/validate/validate.proto";
20 | import "validationtest/import_test.proto";
21 |
22 | message ExampleFieldRules {
23 | string regex_string_field = 1 [(buf.validate.field).string.pattern = "^[a-z0-9]{1,9}$"];
24 | string unconstrained = 2;
25 | }
26 |
27 | message ExampleOneofRules {
28 | // contact_info is the user's contact information
29 | oneof contact_info {
30 | // required ensures that exactly one field in oneof is set. Without this
31 | // option, at most one of email and phone_number is set.
32 | option (buf.validate.oneof).required = true;
33 | // email is the user's email
34 | string email = 1;
35 | // phone_number is the user's phone number.
36 | string phone_number = 2;
37 | }
38 | oneof unconstrained {
39 | string field3 = 3;
40 | string field4 = 4;
41 | }
42 | }
43 |
44 | message ExampleMessageRules {
45 | option (buf.validate.message).cel = {
46 | id: "secondary_email_depends_on_primary"
47 | expression:
48 | "has(this.secondary_email) && !has(this.primary_email)"
49 | "? 'cannot set a secondary email without setting a primary one'"
50 | ": ''"
51 | };
52 | string primary_email = 1;
53 | string secondary_email = 2;
54 | }
55 |
56 | message FieldExpressionMultiple {
57 | string val = 1 [
58 | (buf.validate.field).string.max_len = 5,
59 | (buf.validate.field).string.pattern = "^[a-z0-9]$"
60 | ];
61 | }
62 |
63 | message FieldExpressionMapInt32 {
64 | map val = 1 [(buf.validate.field).cel = {
65 | id: "field_expression.map.int32"
66 | message: "all map values must equal 1"
67 | expression: "this.all(k, this[k] == 1)"
68 | }];
69 | }
70 |
71 | message ExampleImportMessage {
72 | option (buf.validate.message) = {
73 | cel: {
74 | id: "imported_submessage_must_not_be_null"
75 | expression: "this.imported_submessage != null"
76 | }
77 | cel: {
78 | id: "hex_string_must_not_be_empty"
79 | expression: "this.imported_submessage.hex_string != ''"
80 | }
81 | };
82 | ExampleImportedMessage imported_submessage = 1;
83 | }
84 |
85 | message ExampleImportMessageFieldRule {
86 | ExampleImportMessage message_with_import = 1 [
87 | (buf.validate.field).cel = {
88 | id: "field_must_not_be_null"
89 | expression: "this.imported_submessage != null"
90 | },
91 | (buf.validate.field).cel = {
92 | id: "field_string_must_not_be_empty"
93 | expression: "this.imported_submessage.hex_string != ''"
94 | }
95 | ];
96 | }
97 |
98 | message ExampleImportMessageInMap {
99 | option (buf.validate.message) = {
100 | cel: {
101 | id: "imported_submessage_must_not_be_null"
102 | expression: "this.imported_submessage[0] != null"
103 | }
104 | cel: {
105 | id: "hex_string_must_not_be_empty"
106 | expression: "this.imported_submessage[0].hex_string != ''"
107 | }
108 | };
109 | map imported_submessage = 1;
110 | }
111 |
112 | message ExampleImportMessageInMapFieldRule {
113 | ExampleImportMessageInMap message_with_import = 1 [
114 | (buf.validate.field).cel = {
115 | id: "field_must_not_be_null"
116 | expression: "this.imported_submessage[0] != null"
117 | },
118 | (buf.validate.field).cel = {
119 | id: "field_string_must_not_be_empty"
120 | expression: "this.imported_submessage[0].hex_string != ''"
121 | }
122 | ];
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/java/build/buf/protovalidate/ProtoAdapter.java:
--------------------------------------------------------------------------------
1 | // Copyright 2023-2025 Buf Technologies, Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package build.buf.protovalidate;
16 |
17 | import com.google.common.primitives.UnsignedLong;
18 | import com.google.protobuf.AbstractMessage;
19 | import com.google.protobuf.ByteString;
20 | import com.google.protobuf.Descriptors;
21 | import com.google.protobuf.Message;
22 | import com.google.protobuf.Timestamp;
23 | import dev.cel.common.values.CelByteString;
24 | import java.time.Duration;
25 | import java.time.Instant;
26 | import java.util.ArrayList;
27 | import java.util.Collections;
28 | import java.util.HashMap;
29 | import java.util.List;
30 | import java.util.Map;
31 |
32 | /**
33 | * CEL supports protobuf natively but when we pass it field values (like scalars, repeated, and
34 | * maps) it has no way to treat them like a proto message field. This class has methods to convert
35 | * to a cel values.
36 | */
37 | final class ProtoAdapter {
38 | /** Converts a protobuf field value to CEL compatible value. */
39 | static Object toCel(Descriptors.FieldDescriptor fieldDescriptor, Object value) {
40 | Descriptors.FieldDescriptor.Type type = fieldDescriptor.getType();
41 | if (fieldDescriptor.isMapField()) {
42 | List input =
43 | value instanceof List
44 | ? (List) value
45 | : Collections.singletonList((AbstractMessage) value);
46 | Descriptors.FieldDescriptor keyDesc = fieldDescriptor.getMessageType().findFieldByNumber(1);
47 | Descriptors.FieldDescriptor valDesc = fieldDescriptor.getMessageType().findFieldByNumber(2);
48 | Map