gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }