blob: 2fcd7af3c40eda308f81f989d4ede80de40fb6cd [file] [log] [blame]
Abhay Kumara2ae5992025-11-10 14:02:24 +00001package runtime
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io"
8 "sort"
9
10 "google.golang.org/protobuf/proto"
11 "google.golang.org/protobuf/reflect/protoreflect"
12 field_mask "google.golang.org/protobuf/types/known/fieldmaskpb"
13)
14
15func getFieldByName(fields protoreflect.FieldDescriptors, name string) protoreflect.FieldDescriptor {
16 fd := fields.ByName(protoreflect.Name(name))
17 if fd != nil {
18 return fd
19 }
20
21 return fields.ByJSONName(name)
22}
23
24// FieldMaskFromRequestBody creates a FieldMask printing all complete paths from the JSON body.
25func FieldMaskFromRequestBody(r io.Reader, msg proto.Message) (*field_mask.FieldMask, error) {
26 fm := &field_mask.FieldMask{}
27 var root interface{}
28
29 if err := json.NewDecoder(r).Decode(&root); err != nil {
30 if errors.Is(err, io.EOF) {
31 return fm, nil
32 }
33 return nil, err
34 }
35
36 queue := []fieldMaskPathItem{{node: root, msg: msg.ProtoReflect()}}
37 for len(queue) > 0 {
38 // dequeue an item
39 item := queue[0]
40 queue = queue[1:]
41
42 m, ok := item.node.(map[string]interface{})
43 switch {
44 case ok && len(m) > 0:
45 // if the item is an object, then enqueue all of its children
46 for k, v := range m {
47 if item.msg == nil {
48 return nil, errors.New("JSON structure did not match request type")
49 }
50
51 fd := getFieldByName(item.msg.Descriptor().Fields(), k)
52 if fd == nil {
53 return nil, fmt.Errorf("could not find field %q in %q", k, item.msg.Descriptor().FullName())
54 }
55
56 if isDynamicProtoMessage(fd.Message()) {
57 for _, p := range buildPathsBlindly(string(fd.FullName().Name()), v) {
58 newPath := p
59 if item.path != "" {
60 newPath = item.path + "." + newPath
61 }
62 queue = append(queue, fieldMaskPathItem{path: newPath})
63 }
64 continue
65 }
66
67 if isProtobufAnyMessage(fd.Message()) && !fd.IsList() {
68 _, hasTypeField := v.(map[string]interface{})["@type"]
69 if hasTypeField {
70 queue = append(queue, fieldMaskPathItem{path: k})
71 continue
72 } else {
73 return nil, fmt.Errorf("could not find field @type in %q in message %q", k, item.msg.Descriptor().FullName())
74 }
75
76 }
77
78 child := fieldMaskPathItem{
79 node: v,
80 }
81 if item.path == "" {
82 child.path = string(fd.FullName().Name())
83 } else {
84 child.path = item.path + "." + string(fd.FullName().Name())
85 }
86
87 switch {
88 case fd.IsList(), fd.IsMap():
89 // As per: https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/field_mask.proto#L85-L86
90 // Do not recurse into repeated fields. The repeated field goes on the end of the path and we stop.
91 fm.Paths = append(fm.Paths, child.path)
92 case fd.Message() != nil:
93 child.msg = item.msg.Get(fd).Message()
94 fallthrough
95 default:
96 queue = append(queue, child)
97 }
98 }
99 case ok && len(m) == 0:
100 fallthrough
101 case len(item.path) > 0:
102 // otherwise, it's a leaf node so print its path
103 fm.Paths = append(fm.Paths, item.path)
104 }
105 }
106
107 // Sort for deterministic output in the presence
108 // of repeated fields.
109 sort.Strings(fm.Paths)
110
111 return fm, nil
112}
113
114func isProtobufAnyMessage(md protoreflect.MessageDescriptor) bool {
115 return md != nil && (md.FullName() == "google.protobuf.Any")
116}
117
118func isDynamicProtoMessage(md protoreflect.MessageDescriptor) bool {
119 return md != nil && (md.FullName() == "google.protobuf.Struct" || md.FullName() == "google.protobuf.Value")
120}
121
122// buildPathsBlindly does not attempt to match proto field names to the
123// json value keys. Instead it relies completely on the structure of
124// the unmarshalled json contained within in.
125// Returns a slice containing all subpaths with the root at the
126// passed in name and json value.
127func buildPathsBlindly(name string, in interface{}) []string {
128 m, ok := in.(map[string]interface{})
129 if !ok {
130 return []string{name}
131 }
132
133 var paths []string
134 queue := []fieldMaskPathItem{{path: name, node: m}}
135 for len(queue) > 0 {
136 cur := queue[0]
137 queue = queue[1:]
138
139 m, ok := cur.node.(map[string]interface{})
140 if !ok {
141 // This should never happen since we should always check that we only add
142 // nodes of type map[string]interface{} to the queue.
143 continue
144 }
145 for k, v := range m {
146 if mi, ok := v.(map[string]interface{}); ok {
147 queue = append(queue, fieldMaskPathItem{path: cur.path + "." + k, node: mi})
148 } else {
149 // This is not a struct, so there are no more levels to descend.
150 curPath := cur.path + "." + k
151 paths = append(paths, curPath)
152 }
153 }
154 }
155 return paths
156}
157
158// fieldMaskPathItem stores an in-progress deconstruction of a path for a fieldmask
159type fieldMaskPathItem struct {
160 // the list of prior fields leading up to node connected by dots
161 path string
162
163 // a generic decoded json object the current item to inspect for further path extraction
164 node interface{}
165
166 // parent message
167 msg protoreflect.Message
168}