Skip to content

Commit 636af52

Browse files
authored
Implement MessageOneofRule (#308)
Signed-off-by: Sri Krishna <skrishna@buf.build>
1 parent 4dcbaa4 commit 636af52

File tree

4 files changed

+182
-16
lines changed

4 files changed

+182
-16
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Version of buf.build/bufbuild/protovalidate to use.
2-
protovalidate.version = v0.12.0
2+
protovalidate.version = v1.0.0-rc.2
33

44
# Arguments to the protovalidate-conformance CLI
55
protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml

src/main/java/build/buf/protovalidate/EvaluatorBuilder.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import build.buf.validate.FieldPathElement;
2020
import build.buf.validate.FieldRules;
2121
import build.buf.validate.Ignore;
22+
import build.buf.validate.MessageOneofRule;
2223
import build.buf.validate.MessageRules;
2324
import build.buf.validate.OneofRules;
2425
import build.buf.validate.Rule;
@@ -176,6 +177,7 @@ private void buildMessage(Descriptor desc, MessageEvaluator msgEval)
176177
return;
177178
}
178179
processMessageExpressions(descriptor, msgRules, msgEval, defaultInstance);
180+
processMessageOneofRules(descriptor, msgRules, msgEval);
179181
processOneofRules(descriptor, msgEval);
180182
processFields(descriptor, msgEval);
181183
} catch (InvalidProtocolBufferException e) {
@@ -203,6 +205,23 @@ private void processMessageExpressions(
203205
msgEval.append(new CelPrograms(null, compiledPrograms));
204206
}
205207

208+
private void processMessageOneofRules(
209+
Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval)
210+
throws CompilationException {
211+
for (MessageOneofRule rule : msgRules.getOneofList()) {
212+
List<FieldDescriptor> fields = new ArrayList<>(rule.getFieldsCount());
213+
for (String name : rule.getFieldsList()) {
214+
FieldDescriptor field = desc.findFieldByName(name);
215+
if (field == null) {
216+
throw new CompilationException(
217+
String.format("field \"%s\" not found in %s", name, desc.getFullName()));
218+
}
219+
fields.add(field);
220+
}
221+
msgEval.append(new MessageOneofEvaluator(fields, rule.getRequired()));
222+
}
223+
}
224+
206225
private void processOneofRules(Descriptor desc, MessageEvaluator msgEval)
207226
throws InvalidProtocolBufferException, CompilationException {
208227
List<Descriptors.OneofDescriptor> oneofs = desc.getOneofs();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 build.buf.protovalidate;
16+
17+
import build.buf.protovalidate.exceptions.ExecutionException;
18+
import com.google.protobuf.Descriptors.FieldDescriptor;
19+
import com.google.protobuf.Message;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
24+
/**
25+
* A specialized {@link Evaluator} for applying {@code buf.validate.MessageOneofRule} to a {@link
26+
* com.google.protobuf.Message}.
27+
*/
28+
class MessageOneofEvaluator implements Evaluator {
29+
/** List of fields that are part of the oneof */
30+
public final List<FieldDescriptor> fields;
31+
32+
/** If at least one must be set. */
33+
public final boolean required;
34+
35+
MessageOneofEvaluator(List<FieldDescriptor> fields, boolean required) {
36+
this.fields = fields;
37+
this.required = required;
38+
}
39+
40+
@Override
41+
public boolean tautology() {
42+
return false;
43+
}
44+
45+
@Override
46+
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
47+
throws ExecutionException {
48+
Message msg = val.messageValue();
49+
if (msg == null) {
50+
return RuleViolation.NO_VIOLATIONS;
51+
}
52+
int hasCount = 0;
53+
for (FieldDescriptor field : fields) {
54+
if (msg.hasField(field)) {
55+
hasCount++;
56+
}
57+
}
58+
if (hasCount > 1) {
59+
return Collections.singletonList(
60+
RuleViolation.newBuilder()
61+
.setRuleId("message.oneof")
62+
.setMessage(String.format("only one of %s can be set", fieldNames())));
63+
}
64+
if (this.required && hasCount == 0) {
65+
return Collections.singletonList(
66+
RuleViolation.newBuilder()
67+
.setRuleId("message.oneof")
68+
.setMessage(String.format("one of %s must be set", fieldNames())));
69+
}
70+
return Collections.emptyList();
71+
}
72+
73+
String fieldNames() {
74+
return fields.stream().map(FieldDescriptor::getName).collect(Collectors.joining(", "));
75+
}
76+
}

src/main/resources/buf/validate/validate.proto

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ extend google.protobuf.FieldOptions {
7676
// `Rule` represents a validation rule written in the Common Expression
7777
// Language (CEL) syntax. Each Rule includes a unique identifier, an
7878
// optional error message, and the CEL expression to evaluate. For more
79-
// information on CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
79+
// information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
8080
//
8181
// ```proto
8282
// message Foo {
@@ -121,8 +121,8 @@ message MessageRules {
121121
optional bool disabled = 1;
122122

123123
// `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.
124-
// These rules are written in Common Expression Language (CEL) syntax. For more information on
125-
// CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
124+
// These rules are written in Common Expression Language (CEL) syntax. For more information,
125+
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
126126
//
127127
//
128128
// ```proto
@@ -137,6 +137,46 @@ message MessageRules {
137137
// }
138138
// ```
139139
repeated Rule cel = 3;
140+
141+
// `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields
142+
// of which at most one can be present. If `required` is also specified, then exactly one
143+
// of the specified fields _must_ be present.
144+
//
145+
// This will enforce oneof-like constraints with a few features not provided by
146+
// actual Protobuf oneof declarations:
147+
// 1. Repeated and map fields are allowed in this validation. In a Protobuf oneof,
148+
// only scalar fields are allowed.
149+
// 2. Fields with implicit presence are allowed. In a Protobuf oneof, all member
150+
// fields have explicit presence. This means that, for the purpose of determining
151+
// how many fields are set, explicitly setting such a field to its zero value is
152+
// effectively the same as not setting it at all.
153+
// 3. This will generate validation errors when unmarshalling, even from the binary
154+
// format. With a Protobuf oneof, if multiple fields are present in the serialized
155+
// form, earlier values are usually silently ignored when unmarshalling, with only
156+
// the last field being present when unmarshalling completes.
157+
//
158+
//
159+
// ```proto
160+
// message MyMessage {
161+
// // Only one of `field1` or `field2` _can_ be present in this message.
162+
// option (buf.validate.message).oneof = { fields: ["field1", "field2"] };
163+
// // Only one of `field3` or `field4` _must_ be present in this message.
164+
// option (buf.validate.message).oneof = { fields: ["field3", "field4"], required: true };
165+
// string field1 = 1;
166+
// bytes field2 = 2;
167+
// bool field3 = 3;
168+
// int32 field4 = 4;
169+
// }
170+
// ```
171+
repeated MessageOneofRule oneof = 4;
172+
}
173+
174+
message MessageOneofRule {
175+
// A list of field names to include in the oneof. All field names must be
176+
// defined in the message.
177+
repeated string fields = 1;
178+
// If true, one of the fields specified _must_ be set.
179+
optional bool required = 2;
140180
}
141181

142182
// The `OneofRules` message type enables you to manage rules for
@@ -166,8 +206,8 @@ message OneofRules {
166206
// the field, the correct set should be used to ensure proper validations.
167207
message FieldRules {
168208
// `cel` is a repeated field used to represent a textual expression
169-
// in the Common Expression Language (CEL) syntax. For more information on
170-
// CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
209+
// in the Common Expression Language (CEL) syntax. For more information,
210+
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
171211
//
172212
// ```proto
173213
// message MyMessage {
@@ -184,7 +224,7 @@ message FieldRules {
184224
// described as "serialized in the wire format," which includes:
185225
//
186226
// - the following "nullable" fields must be explicitly set to be considered populated:
187-
// - singular message fields (whose fields may be unpopulated / default values)
227+
// - singular message fields (whose fields may be unpopulated/default values)
188228
// - member fields of a oneof (may be their default value)
189229
// - proto3 optional fields (may be their default value)
190230
// - proto2 scalar fields (both optional and required)
@@ -251,8 +291,8 @@ message FieldRules {
251291
// multiple fields.
252292
message PredefinedRules {
253293
// `cel` is a repeated field used to represent a textual expression
254-
// in the Common Expression Language (CEL) syntax. For more information on
255-
// CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
294+
// in the Common Expression Language (CEL) syntax. For more information,
295+
// [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/).
256296
//
257297
// ```proto
258298
// message MyMessage {
@@ -276,7 +316,7 @@ message PredefinedRules {
276316
// Specifies how FieldRules.ignore behaves. See the documentation for
277317
// FieldRules.required for definitions of "populated" and "nullable".
278318
enum Ignore {
279-
// Validation is only skipped if it's an unpopulated nullable fields.
319+
// Validation is only skipped if it's an unpopulated nullable field.
280320
//
281321
// ```proto
282322
// syntax="proto3";
@@ -3809,7 +3849,7 @@ message StringRules {
38093849
extensions 1000 to max;
38103850
}
38113851

3812-
// WellKnownRegex contain some well-known patterns.
3852+
// KnownRegex contains some well-known patterns.
38133853
enum KnownRegex {
38143854
KNOWN_REGEX_UNSPECIFIED = 0;
38153855

@@ -4816,7 +4856,7 @@ message TimestampRules {
48164856
}
48174857

48184858
// `Violations` is a collection of `Violation` messages. This message type is returned by
4819-
// protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.
4859+
// Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.
48204860
// Each individual violation is represented by a `Violation` message.
48214861
message Violations {
48224862
// `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected.
@@ -4828,11 +4868,42 @@ message Violations {
48284868
// caused the violation, the specific rule that wasn't fulfilled, and a
48294869
// human-readable error message.
48304870
//
4871+
// For example, consider the following message:
4872+
//
4873+
// ```proto
4874+
// message User {
4875+
// int32 age = 1 [(buf.validate.field).cel = {
4876+
// id: "user.age",
4877+
// expression: "this < 18 ? 'User must be at least 18 years old' : ''",
4878+
// }];
4879+
// }
4880+
// ```
4881+
//
4882+
// It could produce the following violation:
4883+
//
48314884
// ```json
48324885
// {
4833-
// "fieldPath": "bar",
4834-
// "ruleId": "foo.bar",
4835-
// "message": "bar must be greater than 0"
4886+
// "ruleId": "user.age",
4887+
// "message": "User must be at least 18 years old",
4888+
// "field": {
4889+
// "elements": [
4890+
// {
4891+
// "fieldNumber": 1,
4892+
// "fieldName": "age",
4893+
// "fieldType": "TYPE_INT32"
4894+
// }
4895+
// ]
4896+
// },
4897+
// "rule": {
4898+
// "elements": [
4899+
// {
4900+
// "fieldNumber": 23,
4901+
// "fieldName": "cel",
4902+
// "fieldType": "TYPE_MESSAGE",
4903+
// "index": "0"
4904+
// }
4905+
// ]
4906+
// }
48364907
// }
48374908
// ```
48384909
message Violation {
@@ -4857,7 +4928,7 @@ message Violation {
48574928
// ```
48584929
optional FieldPath field = 5;
48594930

4860-
// `rule` is a machine-readable path that points to the specific rule rule that failed validation.
4931+
// `rule` is a machine-readable path that points to the specific rule that failed validation.
48614932
// This will be a nested field starting from the FieldRules of the field that failed validation.
48624933
// For custom rules, this will provide the path of the rule, e.g. `cel[0]`.
48634934
//

0 commit comments

Comments
 (0)