github.com/grpc-ecosystem/grpc-gateway/v2@v2.19.1/runtime/fieldmask.go (about) 1 package runtime 2 3 import ( 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 15 func 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. 25 func 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 114 func isProtobufAnyMessage(md protoreflect.MessageDescriptor) bool { 115 return md != nil && (md.FullName() == "google.protobuf.Any") 116 } 117 118 func 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. 127 func 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 a in-progress deconstruction of a path for a fieldmask 159 type 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 }