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