| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 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 an 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 | } |