Skip to content

Commit

Permalink
Always treat groups as message fields (#132)
Browse files Browse the repository at this point in the history
The relative simplicity and obviousness of this fix greatly obscures how
difficult it was to determine :)

Protobuf editions adds a couple "new" features to protobuf that cleverly
re-use old protobuf wire and language features.

In case of expanded repeated field representation, there appears to be
no difference: for repeated fields, protobuf already supported both wire
encodings, and protoreflect already had to treat them the same. Thus,
repeated fields with both encodings work fine with protovalidate-go.

In case of delimited message representation, this uses the old proto2
group encoding. This almost works, because protoreflect treats groups
and submessage fields very similarly, and some of the protovalidate-go
code also already handles groups as expected.

Unfortunately though, there were a few places where groups and messages
were not being treated identically. The failure case is rather obscure:
_almost_ everything works, but some special cases are not taken
correctly with the group encoding. This in and of itself is not
automatically a problem but it can lead to weird issues, most notably
the subtype not being discovered and registered correctly in the dynamic
case.

Although I did not individually test each line change to make sure that
it fixes a specific bug, most of these are definitely needed and fix
behavioral issues, except I am not 100% sure about the pgv migrator
(Probably unnecessary but if proto2 groups work in pgv it seems fine for
good measure.) I will make a separate PR to protovalidate that includes
plenty of new test cases that ensure editions works; most of these pass
on `main` save for some of the delimited submessage ones.

cel-go seems to behave correctly already. I initially suspected a
problem with how it handled delimited submessages, but it was a red
herring.
  • Loading branch information
jchadwick-buf committed Jul 19, 2024
1 parent f204e44 commit ad94103
Show file tree
Hide file tree
Showing 10 changed files with 7,296 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ GOLANGCI_LINT_VERSION ?= v1.59.1
# Set to use a different version of protovalidate-conformance.
# Should be kept in sync with the version referenced in proto/buf.lock and
# 'buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go' in go.mod.
CONFORMANCE_VERSION ?= v0.6.3
CONFORMANCE_VERSION ?= v0.7.1

.PHONY: help
help: ## Describe useful make targets
Expand Down
12 changes: 6 additions & 6 deletions buf.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: b983156c5e994cc9892e0ce3e64e17e0
digest: b5:a02a4a5a0a9306cf1391d17e8811bbd6a0dbd0e0e29ddfc34b9d103311164974472d43627c3d63e6a8abaffd6c7a301c4f7965bbab2dc56f7f7812429a84b812
commit: a6c49f84cc0f4e038680d390392e2ab0
digest: b5:e968392e88ff7915adcbd1635d670b45bff8836ec2415d81fc559ca5470a695dbdc30030bad8bc5764647c731079e9e7bba0023ea25c4e4a1672a7d2561d4a19
- name: buf.build/bufbuild/protovalidate-testing
commit: 09d8555b907f4bfa9e784469b66e2bea
digest: b5:de77b29bc5e88cfa2b0b1fa7b0e12f33d77aefca2821f465c5a54b77696e20919ffb7981cc90ceff41d0a3aab761457030056460bf20129b2dacae5cdcbe21b7
commit: 7c0e0340f14c4d6580c200dcaa9b00d0
digest: b5:f0dacc3a02e3fcd277c3516cd0089e69478374b5ad15fe1032853789668d1d660cac4c6e1776799a66b251b9583ddc320e9db91acf6e8681e31d76ccc1f22ea3
- name: buf.build/envoyproxy/protoc-gen-validate
commit: 71881f09a0c5420a9545a07987a86728
digest: b5:e8a034a81f1d6218f712879269bedac768c9e39452d7a92f83d70923fcd6aa9eb02c81ff6ff337cd0dd9b9fe719d6c4459e655f662ae7112aaa1c2110992afd0
commit: daf171c6cdb54629b5f51e345a79e4dd
digest: b5:c745e1521879f43740230b1df673d0729f55704efefdcfc489d4a0a2d40c92a26cacfeab62813403040a8b180142d53b398c7ca784a065e43823605ee49681de
3 changes: 2 additions & 1 deletion celext/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ func RequiredCELEnvOptions(fieldDesc protoreflect.FieldDescriptor) []cel.EnvOpti
RequiredCELEnvOptions(fieldDesc.MapValue())...,
)
}
if fieldDesc.Kind() == protoreflect.MessageKind {
if fieldDesc.Kind() == protoreflect.MessageKind ||
fieldDesc.Kind() == protoreflect.GroupKind {
return []cel.EnvOption{
cel.Types(dynamicpb.NewMessage(fieldDesc.Message())),
}
Expand Down
3 changes: 2 additions & 1 deletion celext/lookups.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ func ProtoFieldToCELType(fieldDesc protoreflect.FieldDescriptor, generic, forIte
}
}

if fieldDesc.Kind() == protoreflect.MessageKind {
if fieldDesc.Kind() == protoreflect.MessageKind ||
fieldDesc.Kind() == protoreflect.GroupKind {
switch fqn := fieldDesc.Message().FullName(); fqn {
case "google.protobuf.Any":
return cel.AnyType
Expand Down
3 changes: 2 additions & 1 deletion internal/constraints/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ func (c *Cache) getExpectedConstraintDescriptor(
return mapFieldConstraintsDesc, true
case targetFieldDesc.IsList() && !forItems:
return repeatedFieldConstraintsDesc, true
case targetFieldDesc.Kind() == protoreflect.MessageKind:
case targetFieldDesc.Kind() == protoreflect.MessageKind,
targetFieldDesc.Kind() == protoreflect.GroupKind:
expected, ok = expectedWKTConstraints[targetFieldDesc.Message().FullName()]
return expected, ok
default:
Expand Down
19 changes: 15 additions & 4 deletions internal/evaluator/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func (bldr *Builder) processEmbeddedMessage(
valEval *value,
cache MessageCache,
) error {
if fdesc.Kind() != protoreflect.MessageKind ||
if !isMessageField(fdesc) ||
bldr.shouldSkip(rules) ||
fdesc.IsMap() ||
(fdesc.IsList() && !forItems) {
Expand All @@ -326,7 +326,7 @@ func (bldr *Builder) processWrapperConstraints(
valEval *value,
cache MessageCache,
) error {
if fdesc.Kind() != protoreflect.MessageKind ||
if !isMessageField(fdesc) ||
bldr.shouldSkip(rules) ||
fdesc.IsMap() ||
(fdesc.IsList() && !forItems) {
Expand Down Expand Up @@ -374,7 +374,7 @@ func (bldr *Builder) processAnyConstraints(
_ MessageCache,
) error {
if (fdesc.IsList() && !forItems) ||
fdesc.Kind() != protoreflect.MessageKind ||
!isMessageField(fdesc) ||
fdesc.Message().FullName() != "google.protobuf.Any" {
return nil
}
Expand Down Expand Up @@ -488,7 +488,7 @@ func (bldr *Builder) zeroValue(fdesc protoreflect.FieldDescriptor, forItems bool
case forItems && fdesc.IsList():
msg := dynamicpb.NewMessage(fdesc.ContainingMessage())
return msg.Get(fdesc).List().NewElement()
case fdesc.Kind() == protoreflect.MessageKind &&
case isMessageField(fdesc) &&
fdesc.Cardinality() != protoreflect.Repeated:
msg := dynamicpb.NewMessage(fdesc.Message())
return protoreflect.ValueOfMessage(msg)
Expand All @@ -509,3 +509,14 @@ func (c MessageCache) SyncTo(other MessageCache) {
other[k] = v
}
}

// isMessageField returns true if the field descriptor fdesc describes a field
// containing a submessage. Although they are represented differently on the
// wire, group fields are treated like message fields in protoreflect and have
// similar properties. In the 2023 edition of protobuf, message fields with the
// delimited encoding feature will be detected as groups, but should otherwise
// be treated the same.
func isMessageField(fdesc protoreflect.FieldDescriptor) bool {
return fdesc.Kind() == protoreflect.MessageKind ||
fdesc.Kind() == protoreflect.GroupKind
}
Loading

0 comments on commit ad94103

Please sign in to comment.