Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/protovalidate-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"scripts": {
"install-protovalidate-conformance": "node scripts/install-protovalidate-conformance.js",
"generate": "buf generate buf.build/bufbuild/protovalidate-testing:v1.0.0-rc.1",
"generate": "buf generate buf.build/bufbuild/protovalidate-testing:v1.0.0-rc.2",
"postgenerate": "license-header src/gen",
"test": "protovalidate-conformance --strict_message --strict_error --expected_failures=expected-failures.yaml -- tsx src/executor.ts",
"format": "biome format --write",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

180 changes: 137 additions & 43 deletions packages/protovalidate-testing/src/gen/buf/validate/validate_pb.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/protovalidate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ if (result.kind !== "valid") {

> [!NOTE]
>
> This version is compatible with [buf.build/bufbuild/protovalidate](https://buf.build/bufbuild/protovalidate) <!-- upstreamProtovalidateRef -->v1.0.0-rc.1<!-- upstreamProtovalidateRef -->
> This version is compatible with [buf.build/bufbuild/protovalidate](https://buf.build/bufbuild/protovalidate) <!-- upstreamProtovalidateRef -->v1.0.0-rc.2<!-- upstreamProtovalidateRef -->
>
> It requires the Protobuf runtime [@bufbuild/protobuf](https://www.npmjs.com/package/@bufbuild/protobuf).

Expand Down
2 changes: 1 addition & 1 deletion packages/protovalidate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"directory": "packages/protovalidate"
},
"scripts": {
"fetch-proto": "buf export buf.build/bufbuild/protovalidate:v1.0.0-rc.1 --output proto",
"fetch-proto": "buf export buf.build/bufbuild/protovalidate:v1.0.0-rc.2 --output proto",
"postfetch-proto": "license-header proto",
"generate": "buf generate",
"postgenerate": "license-header src/gen",
Expand Down
99 changes: 85 additions & 14 deletions packages/protovalidate/proto/buf/validate/validate.proto
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extend google.protobuf.FieldOptions {
// `Rule` represents a validation rule written in the Common Expression
// Language (CEL) syntax. Each Rule includes a unique identifier, an
// optional error message, and the CEL expression to evaluate. For more
// information on CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
// information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
//
// ```proto
// message Foo {
Expand Down Expand Up @@ -121,8 +121,8 @@ message MessageRules {
optional bool disabled = 1;

// `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.
// These rules are written in Common Expression Language (CEL) syntax. For more information on
// CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
// These rules are written in Common Expression Language (CEL) syntax. For more information,
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
//
//
// ```proto
Expand All @@ -137,6 +137,46 @@ message MessageRules {
// }
// ```
repeated Rule cel = 3;

// `oneof` is a repeated field of type MessageOneofRule that specifies a list of fields
// of which at most one can be present. If `required` is also specified, then exactly one
// of the specified fields _must_ be present.
//
// This will enforce oneof-like constraints with a few features not provided by
// actual Protobuf oneof declarations:
// 1. Repeated and map fields are allowed in this validation. In a Protobuf oneof,
// only scalar fields are allowed.
// 2. Fields with implicit presence are allowed. In a Protobuf oneof, all member
// fields have explicit presence. This means that, for the purpose of determining
// how many fields are set, explicitly setting such a field to its zero value is
// effectively the same as not setting it at all.
// 3. This will generate validation errors when unmarshalling, even from the binary
// format. With a Protobuf oneof, if multiple fields are present in the serialized
// form, earlier values are usually silently ignored when unmarshalling, with only
// the last field being present when unmarshalling completes.
//
//
// ```proto
// message MyMessage {
// // Only one of `field1` or `field2` _can_ be present in this message.
// option (buf.validate.message).oneof = { fields: ["field1", "field2"] };
// // Only one of `field3` or `field4` _must_ be present in this message.
// option (buf.validate.message).oneof = { fields: ["field3", "field4"], required: true };
// string field1 = 1;
// bytes field2 = 2;
// bool field3 = 3;
// int32 field4 = 4;
// }
// ```
repeated MessageOneofRule oneof = 4;
}

message MessageOneofRule {
// A list of field names to include in the oneof. All field names must be
// defined in the message.
repeated string fields = 1;
// If true, one of the fields specified _must_ be set.
optional bool required = 2;
}

// The `OneofRules` message type enables you to manage rules for
Expand Down Expand Up @@ -166,8 +206,8 @@ message OneofRules {
// the field, the correct set should be used to ensure proper validations.
message FieldRules {
// `cel` is a repeated field used to represent a textual expression
// in the Common Expression Language (CEL) syntax. For more information on
// CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
// in the Common Expression Language (CEL) syntax. For more information,
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
//
// ```proto
// message MyMessage {
Expand Down Expand Up @@ -251,8 +291,8 @@ message FieldRules {
// multiple fields.
message PredefinedRules {
// `cel` is a repeated field used to represent a textual expression
// in the Common Expression Language (CEL) syntax. For more information on
// CEL, [see our documentation](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md).
// in the Common Expression Language (CEL) syntax. For more information,
// [see our documentation](https://buf.build/docs/protovalidate/schemas/predefined-rules/).
//
// ```proto
// message MyMessage {
Expand All @@ -276,7 +316,7 @@ message PredefinedRules {
// Specifies how FieldRules.ignore behaves. See the documentation for
// FieldRules.required for definitions of "populated" and "nullable".
enum Ignore {
// Validation is only skipped if it's an unpopulated nullable fields.
// Validation is only skipped if it's an unpopulated nullable field.
//
// ```proto
// syntax="proto3";
Expand Down Expand Up @@ -3809,7 +3849,7 @@ message StringRules {
extensions 1000 to max;
}

// WellKnownRegex contain some well-known patterns.
// KnownRegex contains some well-known patterns.
enum KnownRegex {
KNOWN_REGEX_UNSPECIFIED = 0;

Expand Down Expand Up @@ -4816,7 +4856,7 @@ message TimestampRules {
}

// `Violations` is a collection of `Violation` messages. This message type is returned by
// protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.
// Protovalidate when a proto message fails to meet the requirements set by the `Rule` validation rules.
// Each individual violation is represented by a `Violation` message.
message Violations {
// `violations` is a repeated field that contains all the `Violation` messages corresponding to the violations detected.
Expand All @@ -4828,11 +4868,42 @@ message Violations {
// caused the violation, the specific rule that wasn't fulfilled, and a
// human-readable error message.
//
// For example, consider the following message:
//
// ```proto
// message User {
// int32 age = 1 [(buf.validate.field).cel = {
// id: "user.age",
// expression: "this < 18 ? 'User must be at least 18 years old' : ''",
// }];
// }
// ```
//
// It could produce the following violation:
//
// ```json
// {
// "fieldPath": "bar",
// "ruleId": "foo.bar",
// "message": "bar must be greater than 0"
// "ruleId": "user.age",
// "message": "User must be at least 18 years old",
// "field": {
// "elements": [
// {
// "fieldNumber": 1,
// "fieldName": "age",
// "fieldType": "TYPE_INT32"
// }
// ]
// },
// "rule": {
// "elements": [
// {
// "fieldNumber": 23,
// "fieldName": "cel",
// "fieldType": "TYPE_MESSAGE",
// "index": "0"
// }
// ]
// }
// }
// ```
message Violation {
Expand All @@ -4857,7 +4928,7 @@ message Violation {
// ```
optional FieldPath field = 5;

// `rule` is a machine-readable path that points to the specific rule rule that failed validation.
// `rule` is a machine-readable path that points to the specific rule that failed validation.
// This will be a nested field starting from the FieldRules of the field that failed validation.
// For custom rules, this will provide the path of the rule, e.g. `cel[0]`.
//
Expand Down
2 changes: 1 addition & 1 deletion packages/protovalidate/src/cel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
type Registry,
ScalarType,
} from "@bufbuild/protobuf";
import { type Path, type ReflectMessageGet } from "@bufbuild/protobuf/reflect";
import type { Path, ReflectMessageGet } from "@bufbuild/protobuf/reflect";
import { type Timestamp, timestampNow } from "@bufbuild/protobuf/wkt";
import {
type CelEnv,
Expand Down
1 change: 1 addition & 0 deletions packages/protovalidate/src/eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ void suite("check buf.validate.*Rules fields", () => {
StringRulesSchema.field.strict,
MessageRulesSchema.field.cel,
MessageRulesSchema.field.disabled,
MessageRulesSchema.field.oneof,
OneofRulesSchema.field.required,
FieldRulesSchema.field.cel,
FieldRulesSchema.field.required,
Expand Down
28 changes: 28 additions & 0 deletions packages/protovalidate/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,31 @@ export class EvalAnyRules implements Eval<ReflectMessage> {
return this.in.length + this.notIn.length == 0;
}
}

/**
* buf.validate.MessageOneofRule does not use CEL expressions. This implements custom logic for this rule.
*/
export class EvalMessageOneofRule implements Eval<ReflectMessage> {
constructor(
private readonly fields: DescField[],
private readonly required: boolean,
) {}
eval(val: ReflectMessage, cursor: Cursor) {
const setFields = this.fields.filter((field) => val.isSet(field));
if (setFields.length > 1) {
const oneofNames = this.fields.map((f) => f.name).join(", ");
cursor.violate(
`only one of ${oneofNames} can be set`,
"message.oneof",
[],
);
}
if (this.required && setFields.length == 0) {
const oneofNames = this.fields.map((f) => f.name).join(", ");
cursor.violate(`one of ${oneofNames} must be set`, "message.oneof", []);
}
}
prune(): boolean {
return this.fields.length == 0;
}
}
180 changes: 137 additions & 43 deletions packages/protovalidate/src/gen/buf/validate/validate_pb.ts

Large diffs are not rendered by default.

Loading
Loading