...

Source file src/github.com/grpc-ecosystem/grpc-gateway/v2/runtime/fieldmask.go

Documentation: github.com/grpc-ecosystem/grpc-gateway/v2/runtime

     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  }
   169  

View as plain text