diff --git a/README.md b/README.md index efc3702..df6a694 100644 --- a/README.md +++ b/README.md @@ -460,7 +460,7 @@ Welcome your contribution :) - [x] Field - [x] Field_Cardinality - [x] Field_Kind - - [ ] FieldMask + - [x] FieldMask - [x] FloatValue - [x] Int32Value - [x] Int64Value diff --git a/tests/well-known/plugin/plugin.go b/tests/well-known/plugin/plugin.go index 07170db..0f9e70f 100644 --- a/tests/well-known/plugin/plugin.go +++ b/tests/well-known/plugin/plugin.go @@ -9,6 +9,7 @@ import ( "github.com/runtime-radar/go-plugin/tests/well-known/proto" "github.com/runtime-radar/go-plugin/types/known/durationpb" "github.com/runtime-radar/go-plugin/types/known/emptypb" + "github.com/runtime-radar/go-plugin/types/known/fieldmaskpb" "github.com/runtime-radar/go-plugin/types/known/structpb" "github.com/runtime-radar/go-plugin/types/known/timestamppb" "github.com/runtime-radar/go-plugin/types/known/wrapperspb" @@ -44,6 +45,7 @@ func (p TestPlugin) Test(_ context.Context, request *proto.Request) (*proto.Resp J: wrapperspb.String(request.GetJ().Value + "Value"), K: wrapperspb.UInt32(request.GetK().Value * 2), L: wrapperspb.UInt64(request.GetL().Value * 2), + M: &fieldmaskpb.FieldMask{Paths: append(request.GetM().GetPaths(), "added.by_plugin")}, }, nil } diff --git a/tests/well-known/proto/known.pb.go b/tests/well-known/proto/known.pb.go index 8d8faab..d73f849 100644 --- a/tests/well-known/proto/known.pb.go +++ b/tests/well-known/proto/known.pb.go @@ -10,6 +10,7 @@ import ( context "context" durationpb "github.com/runtime-radar/go-plugin/types/known/durationpb" emptypb "github.com/runtime-radar/go-plugin/types/known/emptypb" + fieldmaskpb "github.com/runtime-radar/go-plugin/types/known/fieldmaskpb" structpb "github.com/runtime-radar/go-plugin/types/known/structpb" timestamppb "github.com/runtime-radar/go-plugin/types/known/timestamppb" wrapperspb "github.com/runtime-radar/go-plugin/types/known/wrapperspb" @@ -45,6 +46,8 @@ type Request struct { J *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=j,proto3" json:"j,omitempty"` K *wrapperspb.UInt32Value `protobuf:"bytes,11,opt,name=k,proto3" json:"k,omitempty"` L *wrapperspb.UInt64Value `protobuf:"bytes,12,opt,name=l,proto3" json:"l,omitempty"` + // field mask + M *fieldmaskpb.FieldMask `protobuf:"bytes,13,opt,name=m,proto3" json:"m,omitempty"` } func (x *Request) ProtoReflect() protoreflect.Message { @@ -135,6 +138,13 @@ func (x *Request) GetL() *wrapperspb.UInt64Value { return nil } +func (x *Request) GetM() *fieldmaskpb.FieldMask { + if x != nil { + return x.M + } + return nil +} + type Response struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -152,6 +162,7 @@ type Response struct { J *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=j,proto3" json:"j,omitempty"` K *wrapperspb.UInt32Value `protobuf:"bytes,11,opt,name=k,proto3" json:"k,omitempty"` L *wrapperspb.UInt64Value `protobuf:"bytes,12,opt,name=l,proto3" json:"l,omitempty"` + M *fieldmaskpb.FieldMask `protobuf:"bytes,13,opt,name=m,proto3" json:"m,omitempty"` } func (x *Response) ProtoReflect() protoreflect.Message { @@ -242,6 +253,13 @@ func (x *Response) GetL() *wrapperspb.UInt64Value { return nil } +func (x *Response) GetM() *fieldmaskpb.FieldMask { + if x != nil { + return x.M + } + return nil +} + // go:plugin type=plugin version=1 type KnownTypesTest interface { Test(context.Context, *Request) (*Response, error) diff --git a/tests/well-known/proto/known.proto b/tests/well-known/proto/known.proto index 380f7b3..e0f5206 100644 --- a/tests/well-known/proto/known.proto +++ b/tests/well-known/proto/known.proto @@ -3,6 +3,7 @@ package greeting; import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; @@ -39,6 +40,9 @@ message Request { google.protobuf.StringValue j = 10; google.protobuf.UInt32Value k = 11; google.protobuf.UInt64Value l = 12; + + // field mask + google.protobuf.FieldMask m = 13; } message Response { @@ -54,4 +58,5 @@ message Response { google.protobuf.StringValue j = 10; google.protobuf.UInt32Value k = 11; google.protobuf.UInt64Value l = 12; + google.protobuf.FieldMask m = 13; } diff --git a/tests/well-known/proto/known_vtproto.pb.go b/tests/well-known/proto/known_vtproto.pb.go index e3e89c8..9971590 100644 --- a/tests/well-known/proto/known_vtproto.pb.go +++ b/tests/well-known/proto/known_vtproto.pb.go @@ -9,6 +9,7 @@ package proto import ( fmt "fmt" durationpb "github.com/runtime-radar/go-plugin/types/known/durationpb" + fieldmaskpb "github.com/runtime-radar/go-plugin/types/known/fieldmaskpb" structpb "github.com/runtime-radar/go-plugin/types/known/structpb" timestamppb "github.com/runtime-radar/go-plugin/types/known/timestamppb" wrapperspb "github.com/runtime-radar/go-plugin/types/known/wrapperspb" @@ -54,6 +55,16 @@ func (m *Request) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.M != nil { + size, err := (*fieldmaskpb.FieldMask)(m.M).MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x6a + } if m.L != nil { size, err := (*wrapperspb.UInt64Value)(m.L).MarshalToSizedBufferVT(dAtA[:i]) if err != nil { @@ -207,6 +218,16 @@ func (m *Response) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.M != nil { + size, err := (*fieldmaskpb.FieldMask)(m.M).MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x6a + } if m.L != nil { size, err := (*wrapperspb.UInt64Value)(m.L).MarshalToSizedBufferVT(dAtA[:i]) if err != nil { @@ -384,6 +405,10 @@ func (m *Request) SizeVT() (n int) { l = (*wrapperspb.UInt64Value)(m.L).SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if m.M != nil { + l = (*fieldmaskpb.FieldMask)(m.M).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -442,6 +467,10 @@ func (m *Response) SizeVT() (n int) { l = (*wrapperspb.UInt64Value)(m.L).SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if m.M != nil { + l = (*fieldmaskpb.FieldMask)(m.M).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -907,6 +936,42 @@ func (m *Request) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 13: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field M", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.M == nil { + m.M = &fieldmaskpb.FieldMask{} + } + if err := (*fieldmaskpb.FieldMask)(m.M).UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -1390,6 +1455,42 @@ func (m *Response) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 13: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field M", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.M == nil { + m.M = &fieldmaskpb.FieldMask{} + } + if err := (*fieldmaskpb.FieldMask)(m.M).UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/tests/well-known/well_known_test.go b/tests/well-known/well_known_test.go index 918c125..04337c9 100644 --- a/tests/well-known/well_known_test.go +++ b/tests/well-known/well_known_test.go @@ -11,6 +11,7 @@ import ( "github.com/runtime-radar/go-plugin/tests/well-known/proto" "github.com/runtime-radar/go-plugin/types/known/durationpb" "github.com/runtime-radar/go-plugin/types/known/emptypb" + "github.com/runtime-radar/go-plugin/types/known/fieldmaskpb" "github.com/runtime-radar/go-plugin/types/known/structpb" "github.com/runtime-radar/go-plugin/types/known/timestamppb" "github.com/runtime-radar/go-plugin/types/known/wrapperspb" @@ -63,6 +64,9 @@ func TestWellKnownTypes(t *testing.T) { J: wrapperspb.String("String"), K: wrapperspb.UInt32(3), L: wrapperspb.UInt64(4), + + // field mask + M: &fieldmaskpb.FieldMask{Paths: []string{"foo", "bar.baz"}}, }) c, err = structpb.NewValue(map[string]interface{}{ @@ -96,6 +100,7 @@ func TestWellKnownTypes(t *testing.T) { J: wrapperspb.String("StringValue"), K: wrapperspb.UInt32(6), L: wrapperspb.UInt64(8), + M: &fieldmaskpb.FieldMask{Paths: []string{"foo", "bar.baz", "added.by_plugin"}}, } assert.Equal(t, want, got) } diff --git a/types/known/fieldmaskpb/field_mask.go b/types/known/fieldmaskpb/field_mask.go new file mode 100644 index 0000000..a203437 --- /dev/null +++ b/types/known/fieldmaskpb/field_mask.go @@ -0,0 +1,181 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// Copyright 2022 Teppei Fukuda. All rights reserved. +// https://developers.google.com/protocol-buffers/ + +// Package fieldmaskpb contains a WASM-friendly copy of +// google.protobuf.FieldMask. The helpers in this file mirror upstream's +// reflection-free operations (Append, Union, Intersect, Normalize, IsValid). +// Reflection-dependent helpers (such as the upstream form of New/Append/IsValid +// that take a proto.Message and validate paths against its descriptor) are +// intentionally omitted because protoreflect cannot be used when plugins are +// compiled to wasip1. +package fieldmaskpb + +import "sort" + +// Append adds the given paths to the FieldMask in order. +// +// No validation is performed. Call IsValid afterwards if the input may be +// untrusted. +func (x *FieldMask) Append(paths ...string) { + x.Paths = append(x.Paths, paths...) +} + +// Union returns a new FieldMask whose paths are the union of the input masks' +// paths. The result is normalized (see Normalize). +func Union(masks ...*FieldMask) *FieldMask { + out := &FieldMask{} + for _, m := range masks { + if m == nil { + continue + } + out.Paths = append(out.Paths, m.Paths...) + } + out.Normalize() + return out +} + +// Intersect returns a new FieldMask containing only paths that are covered by +// every input mask. A path p is covered by a mask if the mask contains p or +// an ancestor of p at a path-segment boundary. The result is normalized. +// +// Calling Intersect with no masks returns an empty FieldMask. +func Intersect(masks ...*FieldMask) *FieldMask { + if len(masks) == 0 { + return &FieldMask{} + } + out := &FieldMask{Paths: append([]string(nil), masks[0].GetPaths()...)} + out.Normalize() + + for _, m := range masks[1:] { + next := append([]string(nil), m.GetPaths()...) + other := &FieldMask{Paths: next} + other.Normalize() + + var merged []string + for _, p := range out.Paths { + if covers(other.Paths, p) { + merged = append(merged, p) + } + } + for _, p := range other.Paths { + if covers(out.Paths, p) { + merged = append(merged, p) + } + } + out.Paths = merged + out.Normalize() + if len(out.Paths) == 0 { + return out + } + } + return out +} + +// Normalize sorts paths lexicographically and removes redundancy: duplicate +// paths are collapsed and any path covered by a parent path is dropped (if +// "foo" is in the mask, "foo.bar" is removed because the parent already +// designates everything beneath it). +func (x *FieldMask) Normalize() { + if x == nil || len(x.Paths) == 0 { + return + } + sort.Strings(x.Paths) + out := x.Paths[:0] + var last string + for i, p := range x.Paths { + if i > 0 && p == last { + continue + } + if last != "" && isDescendant(last, p) { + continue + } + out = append(out, p) + last = p + } + x.Paths = out +} + +// IsValid reports whether every path in the mask is syntactically well-formed: +// non-empty, dot-separated, with each segment matching [a-z][a-z0-9_]* (the +// proto field naming convention). +// +// NOTE: This differs from google.golang.org/protobuf/types/known/fieldmaskpb. +// Upstream's IsValid takes a proto.Message and additionally verifies each path +// resolves to an actual field on that message via the message descriptor. That +// descriptor-based check requires protoreflect, which is unavailable when +// plugins are compiled to WASM (wasip1) — the same reason go-plugin ships its +// own copies of the well-known types. Perform descriptor-based validation on +// the host side if you need it. +func (x *FieldMask) IsValid() bool { + if x == nil { + return false + } + for _, p := range x.Paths { + if !isValidPath(p) { + return false + } + } + return true +} + +// isDescendant reports whether child is a strict descendant of parent, where +// child has the form ".". +func isDescendant(parent, child string) bool { + if len(child) <= len(parent) { + return false + } + if child[:len(parent)] != parent { + return false + } + return child[len(parent)] == '.' +} + +// covers reports whether any path in mask is equal to p or a parent of p. +func covers(mask []string, p string) bool { + for _, m := range mask { + if m == p || isDescendant(m, p) { + return true + } + } + return false +} + +// isValidPath reports whether p is a syntactically valid FieldMask path: +// dot-separated, each segment matching [a-z][a-z0-9_]*. +func isValidPath(p string) bool { + if p == "" { + return false + } + segStart := 0 + for i := 0; i <= len(p); i++ { + if i == len(p) || p[i] == '.' { + if !isValidSegment(p[segStart:i]) { + return false + } + segStart = i + 1 + } + } + return true +} + +func isValidSegment(s string) bool { + if s == "" { + return false + } + if s[0] < 'a' || s[0] > 'z' { + return false + } + for i := 1; i < len(s); i++ { + c := s[i] + switch { + case c >= 'a' && c <= 'z': + case c >= '0' && c <= '9': + case c == '_': + default: + return false + } + } + return true +} diff --git a/types/known/fieldmaskpb/field_mask.pb.go b/types/known/fieldmaskpb/field_mask.pb.go new file mode 100644 index 0000000..13f71f4 --- /dev/null +++ b/types/known/fieldmaskpb/field_mask.pb.go @@ -0,0 +1,242 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// Copyright 2022 Teppei Fukuda. All rights reserved. +// https://developers.google.com/protocol-buffers/ + +// Code generated by protoc-gen-go-plugin. DO NOT EDIT. +// versions: +// protoc-gen-go-plugin v0.1.0 +// protoc v7.34.1 +// source: types/known/fieldmaskpb/field_mask.proto + +package fieldmaskpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// `FieldMask` represents a set of symbolic field paths, for example: +// +// paths: "f.a" +// paths: "f.b.d" +// +// Here `f` represents a field in some root message, `a` and `b` +// fields in the message found in `f`, and `d` a field found in the +// message in `f.b`. +// +// Field masks are used to specify a subset of fields that should be +// returned by a get operation or modified by an update operation. +// Field masks also have a custom JSON encoding (see below). +// +// # Field Masks in Projections +// +// When used in the context of a projection, a response message or +// sub-message is filtered by the API to only contain those fields as +// specified in the mask. For example, if the mask in the previous +// example is applied to a response message as follows: +// +// f { +// a : 22 +// b { +// d : 1 +// x : 2 +// } +// y : 13 +// } +// z: 8 +// +// The result will not contain specific values for fields x,y and z +// (their value will be set to the default, and omitted in proto text +// output): +// +// f { +// a : 22 +// b { +// d : 1 +// } +// } +// +// A repeated field is not allowed except at the last position of a +// paths string. +// +// If a FieldMask object is not present in a get operation, the +// operation applies to all fields (as if a FieldMask of all fields +// had been specified). +// +// Note that a field mask does not necessarily apply to the +// top-level response message. In case of a REST get operation, the +// field mask applies directly to the response, but in case of a REST +// list operation, the mask instead applies to each individual message +// in the returned resource list. In case of a REST custom method, +// other definitions may be used. Where the mask applies will be +// clearly documented together with its declaration in the API. In +// any case, the effect on the returned resource/resources is required +// behavior for APIs. +// +// # Field Masks in Update Operations +// +// A field mask in update operations specifies which fields of the +// targeted resource are going to be updated. The API is required +// to only change the values of the fields as specified in the mask +// and leave the others untouched. If a resource is passed in to +// describe the updated values, the API ignores the values of all +// fields not covered by the mask. +// +// If a repeated field is specified for an update operation, new values will +// be appended to the existing repeated field in the target resource. Note that +// a repeated field is only allowed in the last position of a `paths` string. +// +// If a sub-message is specified in the last position of the field mask for an +// update operation, then new value will be merged into the existing +// sub-message in the target resource. +// +// For example, given the target message: +// +// f { +// b { +// d: 1 +// x: 2 +// } +// c: [1] +// } +// +// And an update message: +// +// f { +// b { +// d: 10 +// } +// c: [2] +// } +// +// then if the field mask is: +// +// paths: ["f.b", "f.c"] +// +// then the result will be: +// +// f { +// b { +// d: 10 +// x: 2 +// } +// c: [1, 2] +// } +// +// An implementation may provide options to override this default behavior for +// repeated and message fields. +// +// In order to reset a field's value to the default, the field must +// be in the mask and set to the default value in the provided resource. +// Hence, in order to reset all fields of a resource, provide a default +// instance of the resource and set all fields in the mask, or do +// not provide a mask as described below. +// +// If a field mask is not present on update, the operation applies to +// all fields (as if a field mask of all fields has been specified). +// Note that in the presence of schema evolution, this may mean that +// fields the client does not know and has therefore not filled into +// the request will be reset to their default. If this is unwanted +// behavior, a specific service may require a client to always specify +// a field mask, producing an error if not. +// +// As with get operations, the location of the resource which +// describes the updated values in the request message depends on the +// operation kind. In any case, the effect of the field mask is +// required to be honored by the API. +// +// ## Considerations for HTTP REST +// +// The HTTP kind of an update operation which uses a field mask must +// be set to PATCH instead of PUT in order to satisfy HTTP semantics +// (PUT must only be used for full updates). +// +// # JSON Encoding of Field Masks +// +// In JSON, a field mask is encoded as a single string where paths are +// separated by a comma. Fields name in each path are converted +// to/from lower-camel naming conventions. +// +// As an example, consider the following message declarations: +// +// message Profile { +// User user = 1; +// string photo = 2; +// } +// message User { +// string display_name = 1; +// string address = 2; +// } +// +// In proto a field mask for `Profile` may look as such: +// +// mask { +// paths: "user.display_name" +// paths: "photo" +// } +// +// In JSON, the same mask is represented as below: +// +// { +// mask: "user.displayName,photo" +// } +// +// # Field Masks and Oneof Fields +// +// Field masks treat fields in oneofs just as regular fields. Consider the +// following message: +// +// message SampleMessage { +// oneof test_oneof { +// string name = 4; +// SubMessage sub_message = 9; +// } +// } +// +// The field mask can be: +// +// mask { +// paths: "name" +// } +// +// Or: +// +// mask { +// paths: "sub_message" +// } +// +// Note that oneof type names ("test_oneof" in this case) cannot be used in +// paths. +// +// ## Field Mask Verification +// +// The implementation of any API method which has a FieldMask type field in the +// request should verify the included field paths, and return an +// `INVALID_ARGUMENT` error if any path is unmappable. +type FieldMask struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The set of field mask paths. + Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` +} + +func (x *FieldMask) ProtoReflect() protoreflect.Message { + panic(`not implemented`) +} + +func (x *FieldMask) GetPaths() []string { + if x != nil { + return x.Paths + } + return nil +} diff --git a/types/known/fieldmaskpb/field_mask.proto b/types/known/fieldmaskpb/field_mask.proto new file mode 100644 index 0000000..60a0899 --- /dev/null +++ b/types/known/fieldmaskpb/field_mask.proto @@ -0,0 +1,214 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// Copyright 2022 Teppei Fukuda. All rights reserved. +// https://developers.google.com/protocol-buffers/ + +syntax = "proto3"; + +package google.protobuf; + +option go_package = "github.com/runtime-radar/go-plugin/types/known/fieldmaskpb"; + +// `FieldMask` represents a set of symbolic field paths, for example: +// +// paths: "f.a" +// paths: "f.b.d" +// +// Here `f` represents a field in some root message, `a` and `b` +// fields in the message found in `f`, and `d` a field found in the +// message in `f.b`. +// +// Field masks are used to specify a subset of fields that should be +// returned by a get operation or modified by an update operation. +// Field masks also have a custom JSON encoding (see below). +// +// # Field Masks in Projections +// +// When used in the context of a projection, a response message or +// sub-message is filtered by the API to only contain those fields as +// specified in the mask. For example, if the mask in the previous +// example is applied to a response message as follows: +// +// f { +// a : 22 +// b { +// d : 1 +// x : 2 +// } +// y : 13 +// } +// z: 8 +// +// The result will not contain specific values for fields x,y and z +// (their value will be set to the default, and omitted in proto text +// output): +// +// +// f { +// a : 22 +// b { +// d : 1 +// } +// } +// +// A repeated field is not allowed except at the last position of a +// paths string. +// +// If a FieldMask object is not present in a get operation, the +// operation applies to all fields (as if a FieldMask of all fields +// had been specified). +// +// Note that a field mask does not necessarily apply to the +// top-level response message. In case of a REST get operation, the +// field mask applies directly to the response, but in case of a REST +// list operation, the mask instead applies to each individual message +// in the returned resource list. In case of a REST custom method, +// other definitions may be used. Where the mask applies will be +// clearly documented together with its declaration in the API. In +// any case, the effect on the returned resource/resources is required +// behavior for APIs. +// +// # Field Masks in Update Operations +// +// A field mask in update operations specifies which fields of the +// targeted resource are going to be updated. The API is required +// to only change the values of the fields as specified in the mask +// and leave the others untouched. If a resource is passed in to +// describe the updated values, the API ignores the values of all +// fields not covered by the mask. +// +// If a repeated field is specified for an update operation, new values will +// be appended to the existing repeated field in the target resource. Note that +// a repeated field is only allowed in the last position of a `paths` string. +// +// If a sub-message is specified in the last position of the field mask for an +// update operation, then new value will be merged into the existing +// sub-message in the target resource. +// +// For example, given the target message: +// +// f { +// b { +// d: 1 +// x: 2 +// } +// c: [1] +// } +// +// And an update message: +// +// f { +// b { +// d: 10 +// } +// c: [2] +// } +// +// then if the field mask is: +// +// paths: ["f.b", "f.c"] +// +// then the result will be: +// +// f { +// b { +// d: 10 +// x: 2 +// } +// c: [1, 2] +// } +// +// An implementation may provide options to override this default behavior for +// repeated and message fields. +// +// In order to reset a field's value to the default, the field must +// be in the mask and set to the default value in the provided resource. +// Hence, in order to reset all fields of a resource, provide a default +// instance of the resource and set all fields in the mask, or do +// not provide a mask as described below. +// +// If a field mask is not present on update, the operation applies to +// all fields (as if a field mask of all fields has been specified). +// Note that in the presence of schema evolution, this may mean that +// fields the client does not know and has therefore not filled into +// the request will be reset to their default. If this is unwanted +// behavior, a specific service may require a client to always specify +// a field mask, producing an error if not. +// +// As with get operations, the location of the resource which +// describes the updated values in the request message depends on the +// operation kind. In any case, the effect of the field mask is +// required to be honored by the API. +// +// ## Considerations for HTTP REST +// +// The HTTP kind of an update operation which uses a field mask must +// be set to PATCH instead of PUT in order to satisfy HTTP semantics +// (PUT must only be used for full updates). +// +// # JSON Encoding of Field Masks +// +// In JSON, a field mask is encoded as a single string where paths are +// separated by a comma. Fields name in each path are converted +// to/from lower-camel naming conventions. +// +// As an example, consider the following message declarations: +// +// message Profile { +// User user = 1; +// string photo = 2; +// } +// message User { +// string display_name = 1; +// string address = 2; +// } +// +// In proto a field mask for `Profile` may look as such: +// +// mask { +// paths: "user.display_name" +// paths: "photo" +// } +// +// In JSON, the same mask is represented as below: +// +// { +// mask: "user.displayName,photo" +// } +// +// # Field Masks and Oneof Fields +// +// Field masks treat fields in oneofs just as regular fields. Consider the +// following message: +// +// message SampleMessage { +// oneof test_oneof { +// string name = 4; +// SubMessage sub_message = 9; +// } +// } +// +// The field mask can be: +// +// mask { +// paths: "name" +// } +// +// Or: +// +// mask { +// paths: "sub_message" +// } +// +// Note that oneof type names ("test_oneof" in this case) cannot be used in +// paths. +// +// ## Field Mask Verification +// +// The implementation of any API method which has a FieldMask type field in the +// request should verify the included field paths, and return an +// `INVALID_ARGUMENT` error if any path is unmappable. +message FieldMask { + // The set of field mask paths. + repeated string paths = 1; +} diff --git a/types/known/fieldmaskpb/field_mask_test.go b/types/known/fieldmaskpb/field_mask_test.go new file mode 100644 index 0000000..c562019 --- /dev/null +++ b/types/known/fieldmaskpb/field_mask_test.go @@ -0,0 +1,230 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// Copyright 2022 Teppei Fukuda. All rights reserved. +// https://developers.google.com/protocol-buffers/ + +package fieldmaskpb_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/runtime-radar/go-plugin/types/known/fieldmaskpb" +) + +func TestAppend(t *testing.T) { + t.Run("appends in order", func(t *testing.T) { + m := &fieldmaskpb.FieldMask{} + m.Append("a", "b.c") + m.Append("d") + assert.Equal(t, []string{"a", "b.c", "d"}, m.Paths) + }) + + t.Run("no-op when given nothing", func(t *testing.T) { + m := &fieldmaskpb.FieldMask{Paths: []string{"x"}} + m.Append() + assert.Equal(t, []string{"x"}, m.Paths) + }) +} + +func TestUnion(t *testing.T) { + tests := []struct { + name string + in []*fieldmaskpb.FieldMask + want []string + }{ + { + name: "empty input", + in: nil, + want: nil, + }, + { + name: "deduplicates and sorts", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"a", "b"}}, + {Paths: []string{"b", "c"}}, + }, + want: []string{"a", "b", "c"}, + }, + { + name: "tolerates nil masks", + in: []*fieldmaskpb.FieldMask{ + nil, + {Paths: []string{"x"}}, + nil, + }, + want: []string{"x"}, + }, + { + name: "parent absorbs child", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"foo"}}, + {Paths: []string{"foo.bar"}}, + }, + want: []string{"foo"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fieldmaskpb.Union(tt.in...) + assert.Equal(t, tt.want, got.Paths) + }) + } +} + +func TestIntersect(t *testing.T) { + tests := []struct { + name string + in []*fieldmaskpb.FieldMask + want []string + }{ + { + name: "no input", + in: nil, + want: nil, + }, + { + name: "single mask returns its normalized self", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"b", "a", "a"}}, + }, + want: []string{"a", "b"}, + }, + { + name: "common paths only", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"a", "b", "c"}}, + {Paths: []string{"b", "c", "d"}}, + }, + want: []string{"b", "c"}, + }, + { + name: "empty when one mask is empty", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"a"}}, + {Paths: []string{}}, + }, + want: nil, + }, + { + name: "child intersects parent — most-specific wins", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"foo.bar", "user"}}, + {Paths: []string{"foo", "user.email"}}, + }, + want: []string{"foo.bar", "user.email"}, + }, + { + name: "disjoint masks", + in: []*fieldmaskpb.FieldMask{ + {Paths: []string{"a"}}, + {Paths: []string{"b"}}, + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fieldmaskpb.Intersect(tt.in...) + assert.Equal(t, tt.want, got.Paths) + }) + } +} + +func TestNormalize(t *testing.T) { + tests := []struct { + name string + in []string + want []string + }{ + {name: "empty", in: nil, want: nil}, + {name: "sorts", in: []string{"b", "a"}, want: []string{"a", "b"}}, + {name: "dedupes", in: []string{"a", "a", "b"}, want: []string{"a", "b"}}, + { + name: "parent covers child", + in: []string{"foo.bar", "foo"}, + want: []string{"foo"}, + }, + { + name: "parent only covers descendants at boundary", + in: []string{"foo", "foobar"}, + want: []string{"foo", "foobar"}, + }, + { + name: "deeply nested redundancy", + in: []string{"a.b.c.d", "a.b", "a.b.c"}, + want: []string{"a.b"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &fieldmaskpb.FieldMask{Paths: tt.in} + m.Normalize() + assert.Equal(t, tt.want, m.Paths) + }) + } + + t.Run("nil receiver is safe", func(t *testing.T) { + var m *fieldmaskpb.FieldMask + m.Normalize() + }) +} + +func TestIsValid(t *testing.T) { + t.Run("valid paths", func(t *testing.T) { + m := &fieldmaskpb.FieldMask{Paths: []string{ + "a", + "foo", + "foo.bar", + "a.b.c", + "name_with_underscore", + "x1.y2", + }} + assert.True(t, m.IsValid()) + }) + + cases := []struct { + name string + path string + }{ + {"empty path", ""}, + {"capital letter", "Foo"}, + {"capital in segment", "foo.Bar"}, + {"double dot", "foo..bar"}, + {"trailing dot", "foo."}, + {"leading dot", ".foo"}, + {"leading digit", "9foo"}, + {"hyphen", "foo-bar"}, + {"space", "foo bar"}, + {"underscore-only segment is invalid", "_foo"}, + } + for _, tt := range cases { + t.Run("rejects "+tt.name, func(t *testing.T) { + m := &fieldmaskpb.FieldMask{Paths: []string{tt.path}} + assert.False(t, m.IsValid(), "path: %q", tt.path) + }) + } + + t.Run("nil receiver is invalid", func(t *testing.T) { + var m *fieldmaskpb.FieldMask + assert.False(t, m.IsValid()) + }) + + t.Run("empty mask is valid", func(t *testing.T) { + m := &fieldmaskpb.FieldMask{} + assert.True(t, m.IsValid()) + }) +} + +func TestGetPaths(t *testing.T) { + t.Run("nil receiver returns nil", func(t *testing.T) { + var m *fieldmaskpb.FieldMask + assert.Nil(t, m.GetPaths()) + }) + + t.Run("returns underlying slice", func(t *testing.T) { + m := &fieldmaskpb.FieldMask{Paths: []string{"a", "b"}} + assert.Equal(t, []string{"a", "b"}, m.GetPaths()) + }) +} diff --git a/types/known/fieldmaskpb/field_mask_vtproto.pb.go b/types/known/fieldmaskpb/field_mask_vtproto.pb.go new file mode 100644 index 0000000..5597351 --- /dev/null +++ b/types/known/fieldmaskpb/field_mask_vtproto.pb.go @@ -0,0 +1,168 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// Copyright 2022 Teppei Fukuda. All rights reserved. +// https://developers.google.com/protocol-buffers/ + +// Code generated by protoc-gen-go-plugin. DO NOT EDIT. +// versions: +// protoc-gen-go-plugin v0.1.0 +// protoc v7.34.1 +// source: types/known/fieldmaskpb/field_mask.proto + +package fieldmaskpb + +import ( + fmt "fmt" + protohelpers "github.com/runtime-radar/vtprotobuf/protohelpers" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + io "io" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +func (m *FieldMask) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *FieldMask) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *FieldMask) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Paths) > 0 { + for iNdEx := len(m.Paths) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Paths[iNdEx]) + copy(dAtA[i:], m.Paths[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Paths[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *FieldMask) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Paths) > 0 { + for _, s := range m.Paths { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *FieldMask) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: FieldMask: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: FieldMask: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Paths", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Paths = append(m.Paths, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +}