package runtime
import (
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
field_mask "google.golang.org/protobuf/types/known/fieldmaskpb"
)
func getFieldByName(fields protoreflect .FieldDescriptors , name string ) protoreflect .FieldDescriptor {
fd := fields .ByName (protoreflect .Name (name ))
if fd != nil {
return fd
}
return fields .ByJSONName (name )
}
func FieldMaskFromRequestBody (r io .Reader , msg proto .Message ) (*field_mask .FieldMask , error ) {
fm := &field_mask .FieldMask {}
var root interface {}
if err := json .NewDecoder (r ).Decode (&root ); err != nil {
if errors .Is (err , io .EOF ) {
return fm , nil
}
return nil , err
}
queue := []fieldMaskPathItem {{node : root , msg : msg .ProtoReflect ()}}
for len (queue ) > 0 {
item := queue [0 ]
queue = queue [1 :]
m , ok := item .node .(map [string ]interface {})
switch {
case ok && len (m ) > 0 :
for k , v := range m {
if item .msg == nil {
return nil , errors .New ("JSON structure did not match request type" )
}
fd := getFieldByName (item .msg .Descriptor ().Fields (), k )
if fd == nil {
return nil , fmt .Errorf ("could not find field %q in %q" , k , item .msg .Descriptor ().FullName ())
}
if isDynamicProtoMessage (fd .Message ()) {
for _ , p := range buildPathsBlindly (string (fd .FullName ().Name ()), v ) {
newPath := p
if item .path != "" {
newPath = item .path + "." + newPath
}
queue = append (queue , fieldMaskPathItem {path : newPath })
}
continue
}
if isProtobufAnyMessage (fd .Message ()) && !fd .IsList () {
_ , hasTypeField := v .(map [string ]interface {})["@type" ]
if hasTypeField {
queue = append (queue , fieldMaskPathItem {path : k })
continue
} else {
return nil , fmt .Errorf ("could not find field @type in %q in message %q" , k , item .msg .Descriptor ().FullName ())
}
}
child := fieldMaskPathItem {
node : v ,
}
if item .path == "" {
child .path = string (fd .FullName ().Name ())
} else {
child .path = item .path + "." + string (fd .FullName ().Name ())
}
switch {
case fd .IsList (), fd .IsMap ():
fm .Paths = append (fm .Paths , child .path )
case fd .Message () != nil :
child .msg = item .msg .Get (fd ).Message ()
fallthrough
default :
queue = append (queue , child )
}
}
case ok && len (m ) == 0 :
fallthrough
case len (item .path ) > 0 :
fm .Paths = append (fm .Paths , item .path )
}
}
sort .Strings (fm .Paths )
return fm , nil
}
func isProtobufAnyMessage(md protoreflect .MessageDescriptor ) bool {
return md != nil && (md .FullName () == "google.protobuf.Any" )
}
func isDynamicProtoMessage(md protoreflect .MessageDescriptor ) bool {
return md != nil && (md .FullName () == "google.protobuf.Struct" || md .FullName () == "google.protobuf.Value" )
}
func buildPathsBlindly(name string , in interface {}) []string {
m , ok := in .(map [string ]interface {})
if !ok {
return []string {name }
}
var paths []string
queue := []fieldMaskPathItem {{path : name , node : m }}
for len (queue ) > 0 {
cur := queue [0 ]
queue = queue [1 :]
m , ok := cur .node .(map [string ]interface {})
if !ok {
continue
}
for k , v := range m {
if mi , ok := v .(map [string ]interface {}); ok {
queue = append (queue , fieldMaskPathItem {path : cur .path + "." + k , node : mi })
} else {
curPath := cur .path + "." + k
paths = append (paths , curPath )
}
}
}
return paths
}
type fieldMaskPathItem struct {
path string
node interface {}
msg protoreflect .Message
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .