You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
6.0 KiB
221 lines
6.0 KiB
package form |
|
|
|
import ( |
|
"encoding/base64" |
|
"fmt" |
|
"net/url" |
|
"reflect" |
|
"strconv" |
|
"strings" |
|
|
|
"google.golang.org/protobuf/proto" |
|
"google.golang.org/protobuf/reflect/protoreflect" |
|
"google.golang.org/protobuf/types/known/fieldmaskpb" |
|
) |
|
|
|
// EncodeValues encode a message into url values. |
|
func EncodeValues(msg proto.Message) (url.Values, error) { |
|
if msg == nil || (reflect.ValueOf(msg).Kind() == reflect.Ptr && reflect.ValueOf(msg).IsNil()) { |
|
return url.Values{}, nil |
|
} |
|
u := make(url.Values) |
|
err := encodeByField(u, "", msg.ProtoReflect()) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return u, nil |
|
} |
|
|
|
func encodeByField(u url.Values, path string, m protoreflect.Message) (finalErr error) { |
|
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { |
|
var ( |
|
key string |
|
newPath string |
|
) |
|
if fd.HasJSONName() { |
|
key = fd.JSONName() |
|
} else { |
|
key = fd.TextName() |
|
} |
|
if path == "" { |
|
newPath = key |
|
} else { |
|
newPath = path + "." + key |
|
} |
|
if of := fd.ContainingOneof(); of != nil { |
|
if f := m.WhichOneof(of); f != nil { |
|
if f != fd { |
|
return true |
|
} |
|
} |
|
} |
|
switch { |
|
case fd.IsList(): |
|
if v.List().Len() > 0 { |
|
list, err := encodeRepeatedField(fd, v.List()) |
|
if err != nil { |
|
finalErr = err |
|
return false |
|
} |
|
u[newPath] = list |
|
} |
|
case fd.IsMap(): |
|
if v.Map().Len() > 0 { |
|
m, err := encodeMapField(fd, v.Map()) |
|
if err != nil { |
|
finalErr = err |
|
return false |
|
} |
|
for k, value := range m { |
|
u[fmt.Sprintf("%s[%s]", newPath, k)] = []string{value} |
|
} |
|
} |
|
case (fd.Kind() == protoreflect.MessageKind) || (fd.Kind() == protoreflect.GroupKind): |
|
value, err := encodeMessage(fd.Message(), v) |
|
if err == nil { |
|
u[newPath] = []string{value} |
|
return true |
|
} |
|
if err = encodeByField(u, newPath, v.Message()); err != nil { |
|
finalErr = err |
|
return false |
|
} |
|
default: |
|
value, err := EncodeField(fd, v) |
|
if err != nil { |
|
finalErr = err |
|
return false |
|
} |
|
u[newPath] = []string{value} |
|
} |
|
return true |
|
}) |
|
return |
|
} |
|
|
|
func encodeRepeatedField(fieldDescriptor protoreflect.FieldDescriptor, list protoreflect.List) ([]string, error) { |
|
var values []string |
|
for i := 0; i < list.Len(); i++ { |
|
value, err := EncodeField(fieldDescriptor, list.Get(i)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
values = append(values, value) |
|
} |
|
return values, nil |
|
} |
|
|
|
func encodeMapField(fieldDescriptor protoreflect.FieldDescriptor, mp protoreflect.Map) (map[string]string, error) { |
|
m := make(map[string]string) |
|
mp.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool { |
|
key, err := EncodeField(fieldDescriptor.MapValue(), k.Value()) |
|
if err != nil { |
|
return false |
|
} |
|
value, err := EncodeField(fieldDescriptor.MapValue(), v) |
|
if err != nil { |
|
return false |
|
} |
|
m[key] = value |
|
return true |
|
}) |
|
|
|
return m, nil |
|
} |
|
|
|
// EncodeField encode proto message filed |
|
func EncodeField(fieldDescriptor protoreflect.FieldDescriptor, value protoreflect.Value) (string, error) { |
|
switch fieldDescriptor.Kind() { |
|
case protoreflect.BoolKind: |
|
return strconv.FormatBool(value.Bool()), nil |
|
case protoreflect.EnumKind: |
|
if fieldDescriptor.Enum().FullName() == "google.protobuf.NullValue" { |
|
return nullStr, nil |
|
} |
|
desc := fieldDescriptor.Enum().Values().ByNumber(value.Enum()) |
|
return string(desc.Name()), nil |
|
case protoreflect.StringKind: |
|
return value.String(), nil |
|
case protoreflect.BytesKind: |
|
return base64.URLEncoding.EncodeToString(value.Bytes()), nil |
|
case protoreflect.MessageKind, protoreflect.GroupKind: |
|
return encodeMessage(fieldDescriptor.Message(), value) |
|
default: |
|
return fmt.Sprint(value.Interface()), nil |
|
} |
|
} |
|
|
|
// encodeMessage marshals the fields in the given protoreflect.Message. |
|
// If the typeURL is non-empty, then a synthetic "@type" field is injected |
|
// containing the URL as the value. |
|
func encodeMessage(msgDescriptor protoreflect.MessageDescriptor, value protoreflect.Value) (string, error) { |
|
switch msgDescriptor.FullName() { |
|
case timestampMessageFullname: |
|
return marshalTimestamp(value.Message()) |
|
case durationMessageFullname: |
|
return marshalDuration(value.Message()) |
|
case bytesMessageFullname: |
|
return marshalBytes(value.Message()) |
|
case "google.protobuf.DoubleValue", "google.protobuf.FloatValue", "google.protobuf.Int64Value", "google.protobuf.Int32Value", |
|
"google.protobuf.UInt64Value", "google.protobuf.UInt32Value", "google.protobuf.BoolValue", "google.protobuf.StringValue": |
|
fd := msgDescriptor.Fields() |
|
v := value.Message().Get(fd.ByName(protoreflect.Name("value"))) |
|
return fmt.Sprint(v.Interface()), nil |
|
case fieldMaskFullName: |
|
m, ok := value.Message().Interface().(*fieldmaskpb.FieldMask) |
|
if !ok || m == nil { |
|
return "", nil |
|
} |
|
for i, v := range m.Paths { |
|
m.Paths[i] = jsonCamelCase(v) |
|
} |
|
return strings.Join(m.Paths, ","), nil |
|
default: |
|
return "", fmt.Errorf("unsupported message type: %q", string(msgDescriptor.FullName())) |
|
} |
|
} |
|
|
|
// EncodeFieldMask return field mask name=paths |
|
func EncodeFieldMask(m protoreflect.Message) (query string) { |
|
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { |
|
if fd.Kind() == protoreflect.MessageKind { |
|
if msg := fd.Message(); msg.FullName() == fieldMaskFullName { |
|
value, err := encodeMessage(msg, v) |
|
if err != nil { |
|
return false |
|
} |
|
if fd.HasJSONName() { |
|
query = fd.JSONName() + "=" + value |
|
} else { |
|
query = fd.TextName() + "=" + value |
|
} |
|
return false |
|
} |
|
} |
|
return true |
|
}) |
|
return |
|
} |
|
|
|
// JSONCamelCase converts a snake_case identifier to a camelCase identifier, |
|
// according to the protobuf JSON specification. |
|
// references: https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protojson/well_known_types.go#L842 |
|
func jsonCamelCase(s string) string { |
|
var b []byte |
|
var wasUnderscore bool |
|
for i := 0; i < len(s); i++ { // proto identifiers are always ASCII |
|
c := s[i] |
|
if c != '_' { |
|
if wasUnderscore && isASCIILower(c) { |
|
c -= 'a' - 'A' // convert to uppercase |
|
} |
|
b = append(b, c) |
|
} |
|
wasUnderscore = c == '_' |
|
} |
|
return string(b) |
|
} |
|
|
|
func isASCIILower(c byte) bool { |
|
return 'a' <= c && c <= 'z' |
|
}
|
|
|