mirror of https://github.com/hashicorp/consul
509 lines
20 KiB
Go
509 lines
20 KiB
Go
|
package propertyoverride
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"google.golang.org/protobuf/proto"
|
||
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||
|
)
|
||
|
|
||
|
// PatchStruct patches the given ProtoMessage according to the documented behavior of Patch and returns the result.
|
||
|
// If an error is returned, the returned K (which may be a partially modified copy) should be ignored.
|
||
|
func PatchStruct[K proto.Message](k K, patch Patch, debug bool) (result K, e error) {
|
||
|
// Don't panic due to misconfiguration.
|
||
|
defer func() {
|
||
|
if err := recover(); err != nil {
|
||
|
e = fmt.Errorf("unexpected panic: %v", err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
protoM := k.ProtoReflect()
|
||
|
|
||
|
if patch.Path == "" || patch.Path == "/" {
|
||
|
// Return error with all possible fields at root of target.
|
||
|
return k, fmt.Errorf("non-empty, non-root Path is required;\n%s",
|
||
|
fieldListStr(protoM.Descriptor(), debug))
|
||
|
}
|
||
|
|
||
|
parsedPath := parsePath(patch.Path)
|
||
|
targetM, fieldDesc, err := findTargetMessageAndField(protoM, parsedPath, patch, debug)
|
||
|
if err != nil {
|
||
|
return k, err
|
||
|
}
|
||
|
if err := patchField(targetM, fieldDesc, patch, debug); err != nil {
|
||
|
return k, err
|
||
|
}
|
||
|
|
||
|
// ProtoReflect returns a reflective _view_ of the underlying K, so
|
||
|
// we don't need to convert back explictly when we return k here.
|
||
|
return k, nil
|
||
|
}
|
||
|
|
||
|
// parsePath returns the path tokens from a JSON Pointer (https://datatracker.ietf.org/doc/html/rfc6901/) string as
|
||
|
// expected by JSON Patch (e.g. "/foo/bar/baz").
|
||
|
func parsePath(path string) []string {
|
||
|
return strings.Split(strings.TrimLeft(path, "/"), "/")
|
||
|
}
|
||
|
|
||
|
// findTargetMessageAndField takes a root-level protoreflect.Message and slice of path elements and returns the
|
||
|
// protoreflect.FieldDescriptor that corresponds to the full path, along with the parent protoreflect.Message of
|
||
|
// that field. parsedPath must be non-empty.
|
||
|
// If any field in parsedPath cannot be matched while traversing the tree of messages, an error is returned.
|
||
|
func findTargetMessageAndField(m protoreflect.Message, parsedPath []string, patch Patch, debug bool) (protoreflect.Message, protoreflect.FieldDescriptor, error) {
|
||
|
if len(parsedPath) == 0 {
|
||
|
return nil, nil, fmt.Errorf("unexpected error: non-empty path is required")
|
||
|
}
|
||
|
|
||
|
// Iterate until we've matched the (potentially nested) target field.
|
||
|
for {
|
||
|
fieldName := parsedPath[0]
|
||
|
if fieldName == "" {
|
||
|
return nil, nil, fmt.Errorf("empty field name in path")
|
||
|
}
|
||
|
|
||
|
fieldDesc, err := childFieldDescriptor(m.Descriptor(), fieldName, debug)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
parsedPath = parsedPath[1:]
|
||
|
if len(parsedPath) == 0 {
|
||
|
// We've reached the end of the path, return current message and target field.
|
||
|
return m, fieldDesc, nil
|
||
|
}
|
||
|
|
||
|
// Check whether we have a non-terminal (parent) field in the path for which we
|
||
|
// don't support child lookup.
|
||
|
switch {
|
||
|
case fieldDesc.IsList():
|
||
|
return nil, nil, fmt.Errorf("path contains member of repeated field '%s'; repeated field member access is not supported",
|
||
|
fieldName)
|
||
|
case fieldDesc.IsMap():
|
||
|
return nil, nil, fmt.Errorf("path contains member of map field '%s'; map field member access is not supported",
|
||
|
fieldName)
|
||
|
}
|
||
|
|
||
|
fieldM := m.Get(fieldDesc).Message()
|
||
|
if !fieldM.IsValid() && patch.Op == OpAdd {
|
||
|
// Init this message field to a valid, empty value so that we can keep walking
|
||
|
// the path and set inner fields. Only do this for add, as remove should not
|
||
|
// initialize fields on its own.
|
||
|
m.Set(fieldDesc, protoreflect.ValueOfMessage(fieldM.New()))
|
||
|
fieldM = m.Get(fieldDesc).Message()
|
||
|
}
|
||
|
|
||
|
// Advance our parent message "pointer" to the next field.
|
||
|
m = fieldM
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// patchField applies the given patch op to the target field on given parent message.
|
||
|
func patchField(parentM protoreflect.Message, fieldDesc protoreflect.FieldDescriptor, patch Patch, debug bool) error {
|
||
|
switch patch.Op {
|
||
|
case OpAdd:
|
||
|
return applyAdd(parentM, fieldDesc, patch, debug)
|
||
|
case OpRemove:
|
||
|
// Ignore Value if provided, per JSON Patch: "Members that are not explicitly defined for the
|
||
|
// operation in question MUST be ignored (i.e., the operation will complete as if the undefined
|
||
|
// member did not appear in the object)."
|
||
|
return applyRemove(parentM, fieldDesc)
|
||
|
}
|
||
|
return fmt.Errorf("unexpected error: no op implementation found")
|
||
|
}
|
||
|
|
||
|
// removeField clears the target field on the given message.
|
||
|
func applyRemove(parentM protoreflect.Message, fieldDesc protoreflect.FieldDescriptor) error {
|
||
|
// Check whether the parent has this field, as clearing a field on an unset parent may panic.
|
||
|
if parentM.Has(fieldDesc) {
|
||
|
parentM.Clear(fieldDesc)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// applyAdd updates the target field(s) on the given message based on the content of patch.
|
||
|
// If the patch value is a scalar, scalar wrapper, or scalar array, we set the target field directly with that value.
|
||
|
// If the patch value is a map, we set the indicated child fields on a new (empty) message matching the target field.
|
||
|
// Regardless, the target field is replaced entirely. This conforms to the PUT-style semantics of the JSON Patch "add"
|
||
|
// operation for objects (https://www.rfc-editor.org/rfc/rfc6902#section-4.1).
|
||
|
func applyAdd(parentM protoreflect.Message, fieldDesc protoreflect.FieldDescriptor, patch Patch, debug bool) error {
|
||
|
if patch.Value == nil {
|
||
|
return fmt.Errorf("non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields")
|
||
|
}
|
||
|
mapValue, isMapValue := patch.Value.(map[string]interface{})
|
||
|
// If the field is a proto map type, we'll treat it as a "single" field for error handling purposes.
|
||
|
// If we support proto map targets in the future, it will still likely be treated as a single field,
|
||
|
// similar to a list (repeated field). This map handling is specific to _our_ patch semantics for
|
||
|
// updating multiple message fields at once.
|
||
|
if isMapValue && !fieldDesc.IsMap() {
|
||
|
// Get a fresh copy of the target field's message, then set the children indicated by the patch.
|
||
|
fieldM := parentM.Get(fieldDesc).Message().New()
|
||
|
for k, v := range mapValue {
|
||
|
targetFieldDesc, err := childFieldDescriptor(fieldDesc.Message(), k, debug)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
val, err := toProtoValue(fieldM, targetFieldDesc, v)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
fieldM.Set(targetFieldDesc, val)
|
||
|
}
|
||
|
parentM.Set(fieldDesc, protoreflect.ValueOf(fieldM))
|
||
|
} else {
|
||
|
// Just set the field directly, as our patch value is not a map.
|
||
|
val, err := toProtoValue(parentM, fieldDesc, patch.Value)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
parentM.Set(fieldDesc, val)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func childFieldDescriptor(parentDesc protoreflect.MessageDescriptor, fieldName string, debug bool) (protoreflect.FieldDescriptor, error) {
|
||
|
if childFieldDesc := parentDesc.Fields().ByName(protoreflect.Name(fieldName)); childFieldDesc != nil {
|
||
|
return childFieldDesc, nil
|
||
|
}
|
||
|
return nil, fmt.Errorf("no match for field '%s'!\n%s", fieldName, fieldListStr(parentDesc, debug))
|
||
|
}
|
||
|
|
||
|
// fieldListStr prints all possible fields (debug) or the first 10 fields of a given MessageDescriptor.
|
||
|
func fieldListStr(messageDesc protoreflect.MessageDescriptor, debug bool) string {
|
||
|
// Future: it might be nice to use something like https://github.com/agnivade/levenshtein
|
||
|
// or https://github.com/schollz/closestmatch to inform these choices.
|
||
|
fields := messageDesc.Fields()
|
||
|
|
||
|
// If we're not in debug mode and there's > 10 possible fields, only print the first 10.
|
||
|
printCount := fields.Len()
|
||
|
truncateFields := false
|
||
|
if !debug && printCount > 10 {
|
||
|
truncateFields = true
|
||
|
printCount = 10
|
||
|
}
|
||
|
|
||
|
msg := strings.Builder{}
|
||
|
msg.WriteString("available ")
|
||
|
msg.WriteString(string(messageDesc.FullName()))
|
||
|
msg.WriteString(" fields:\n")
|
||
|
for i := 0; i < printCount; i++ {
|
||
|
msg.WriteString(fields.Get(i).TextName())
|
||
|
msg.WriteString("\n")
|
||
|
}
|
||
|
if truncateFields {
|
||
|
msg.WriteString("First 10 fields for this message included, configure with `Debug = true` to print all.")
|
||
|
}
|
||
|
|
||
|
return msg.String()
|
||
|
}
|
||
|
|
||
|
func toProtoValue(parentM protoreflect.Message, fieldDesc protoreflect.FieldDescriptor, patchValue interface{}) (v protoreflect.Value, e error) {
|
||
|
// Repeated fields. Check for these first, so we can use Kind below for single value
|
||
|
// fields (repeated fields have a Kind corresponding to their members).
|
||
|
// We have to do special handling for int types and strings since they could be enums.
|
||
|
if fieldDesc.IsList() {
|
||
|
list := parentM.NewField(fieldDesc).List()
|
||
|
switch val := patchValue.(type) {
|
||
|
case []int:
|
||
|
return toProtoIntOrEnumList(val, list, fieldDesc)
|
||
|
case []int32:
|
||
|
return toProtoIntOrEnumList(val, list, fieldDesc)
|
||
|
case []int64:
|
||
|
return toProtoIntOrEnumList(val, list, fieldDesc)
|
||
|
case []uint:
|
||
|
return toProtoIntOrEnumList(val, list, fieldDesc)
|
||
|
case []uint32:
|
||
|
return toProtoIntOrEnumList(val, list, fieldDesc)
|
||
|
case []uint64:
|
||
|
return toProtoIntOrEnumList(val, list, fieldDesc)
|
||
|
case []float32:
|
||
|
return toProtoNumericList(val, list, fieldDesc)
|
||
|
case []float64:
|
||
|
return toProtoNumericList(val, list, fieldDesc)
|
||
|
case []bool:
|
||
|
return toProtoList(val, list)
|
||
|
case []string:
|
||
|
if fieldDesc.Kind() == protoreflect.EnumKind {
|
||
|
return toProtoEnumList(val, list, fieldDesc)
|
||
|
}
|
||
|
return toProtoList(val, list)
|
||
|
default:
|
||
|
if fieldDesc.Kind() == protoreflect.MessageKind ||
|
||
|
fieldDesc.Kind() == protoreflect.GroupKind ||
|
||
|
fieldDesc.Kind() == protoreflect.BytesKind {
|
||
|
return unsupportedTargetTypeErr(fieldDesc)
|
||
|
}
|
||
|
return typeMismatchErr(fieldDesc, val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch fieldDesc.Kind() {
|
||
|
case protoreflect.MessageKind:
|
||
|
// google.protobuf wrapper types are used for detecting presence of scalars. If the
|
||
|
// target field is a message, and the patch value is not a map, we assume the user
|
||
|
// is targeting a wrapper type or has misconfigured the path.
|
||
|
return toProtoWrapperValue(fieldDesc, patchValue)
|
||
|
case protoreflect.EnumKind:
|
||
|
return toProtoEnumValue(fieldDesc, patchValue)
|
||
|
case protoreflect.Int32Kind,
|
||
|
protoreflect.Int64Kind,
|
||
|
protoreflect.Sint32Kind,
|
||
|
protoreflect.Sint64Kind,
|
||
|
protoreflect.Uint32Kind,
|
||
|
protoreflect.Uint64Kind,
|
||
|
protoreflect.Fixed32Kind,
|
||
|
protoreflect.Fixed64Kind,
|
||
|
protoreflect.Sfixed32Kind,
|
||
|
protoreflect.Sfixed64Kind,
|
||
|
protoreflect.FloatKind,
|
||
|
protoreflect.DoubleKind:
|
||
|
// We have to be careful specifically obtaining the correct proto field type here,
|
||
|
// since conversion checking by protoreflect is stringent and will not accept e.g.
|
||
|
// int->uint32 mismatches, or float->int downcasts.
|
||
|
switch val := patchValue.(type) {
|
||
|
case int:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case int32:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case int64:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case uint:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case uint32:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case uint64:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case float32:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
case float64:
|
||
|
return toProtoNumericValue(fieldDesc, val)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Fall back to protoreflect.ValueOf, which may panic if an unexpected type is passed.
|
||
|
defer func() {
|
||
|
if err := recover(); err != nil {
|
||
|
_, e = typeMismatchErr(fieldDesc, patchValue)
|
||
|
}
|
||
|
}()
|
||
|
return protoreflect.ValueOf(patchValue), nil
|
||
|
}
|
||
|
|
||
|
func toProtoList[V float32 | float64 | bool | string](vs []V, l protoreflect.List) (protoreflect.Value, error) {
|
||
|
for _, v := range vs {
|
||
|
l.Append(protoreflect.ValueOf(v))
|
||
|
}
|
||
|
return protoreflect.ValueOfList(l), nil
|
||
|
}
|
||
|
|
||
|
// toProtoIntOrEnumList takes a slice of some integer type V and returns a protoreflect.Value List of either the
|
||
|
// corresponding proto integer type or enum values, depending on the type of the target field.
|
||
|
func toProtoIntOrEnumList[V int | int32 | int64 | uint | uint32 | uint64](vs []V, l protoreflect.List, fieldDesc protoreflect.FieldDescriptor) (protoreflect.Value, error) {
|
||
|
if fieldDesc.Kind() == protoreflect.EnumKind {
|
||
|
return toProtoEnumList(vs, l, fieldDesc)
|
||
|
}
|
||
|
return toProtoNumericList(vs, l, fieldDesc)
|
||
|
}
|
||
|
|
||
|
// toProtoNumericList takes a slice of some numeric type V and returns a protoreflect.Value List of the corresponding
|
||
|
// proto integer or float values, depending on the type of the target field.
|
||
|
func toProtoNumericList[V int | int32 | int64 | uint | uint32 | uint64 | float32 | float64](vs []V, l protoreflect.List, fieldDesc protoreflect.FieldDescriptor) (protoreflect.Value, error) {
|
||
|
for _, v := range vs {
|
||
|
i, err := toProtoNumericValue(fieldDesc, v)
|
||
|
if err != nil {
|
||
|
return protoreflect.Value{}, err
|
||
|
}
|
||
|
l.Append(i)
|
||
|
}
|
||
|
return protoreflect.ValueOfList(l), nil
|
||
|
}
|
||
|
|
||
|
func toProtoEnumList[V int | int32 | int64 | uint | uint32 | uint64 | string](vs []V, l protoreflect.List, fieldDesc protoreflect.FieldDescriptor) (protoreflect.Value, error) {
|
||
|
for _, v := range vs {
|
||
|
e, err := toProtoEnumValue(fieldDesc, v)
|
||
|
if err != nil {
|
||
|
return protoreflect.Value{}, err
|
||
|
}
|
||
|
l.Append(e)
|
||
|
}
|
||
|
return protoreflect.ValueOfList(l), nil
|
||
|
}
|
||
|
|
||
|
// toProtoNumericValue aids converting from a Go numeric type to a specific protoreflect.Value type, casting to match
|
||
|
// the target field type.
|
||
|
//
|
||
|
// It supports converting numeric Go slices to List values, and scalar numeric values, in a protoreflect
|
||
|
// validation-compatible manner by replacing the internal conversion logic of protoreflect.ValueOf with a more lenient
|
||
|
// "blind" cast, with the assumption that inputs to structpatcher may have been deserialized in such a way as to make
|
||
|
// strict type checking infeasible, even if the actual value and target type are compatible. This function does _not_
|
||
|
// attempt to handle overflows, only type conversion.
|
||
|
//
|
||
|
// See https://protobuf.dev/programming-guides/proto3/#scalar for canonical proto3 type mappings, reflected here.
|
||
|
func toProtoNumericValue[V int | int32 | int64 | uint | uint32 | uint64 | float32 | float64](fieldDesc protoreflect.FieldDescriptor, v V) (protoreflect.Value, error) {
|
||
|
switch fieldDesc.Kind() {
|
||
|
case protoreflect.FloatKind:
|
||
|
return protoreflect.ValueOfFloat32(float32(v)), nil
|
||
|
case protoreflect.DoubleKind:
|
||
|
return protoreflect.ValueOfFloat64(float64(v)), nil
|
||
|
case protoreflect.Int32Kind:
|
||
|
return protoreflect.ValueOfInt32(int32(v)), nil
|
||
|
case protoreflect.Int64Kind:
|
||
|
return protoreflect.ValueOfInt64(int64(v)), nil
|
||
|
case protoreflect.Uint32Kind:
|
||
|
return protoreflect.ValueOfUint32(uint32(v)), nil
|
||
|
case protoreflect.Uint64Kind:
|
||
|
return protoreflect.ValueOfUint64(uint64(v)), nil
|
||
|
case protoreflect.Sint32Kind:
|
||
|
return protoreflect.ValueOfInt32(int32(v)), nil
|
||
|
case protoreflect.Sint64Kind:
|
||
|
return protoreflect.ValueOfInt64(int64(v)), nil
|
||
|
case protoreflect.Fixed32Kind:
|
||
|
return protoreflect.ValueOfUint32(uint32(v)), nil
|
||
|
case protoreflect.Fixed64Kind:
|
||
|
return protoreflect.ValueOfUint64(uint64(v)), nil
|
||
|
case protoreflect.Sfixed32Kind:
|
||
|
return protoreflect.ValueOfInt32(int32(v)), nil
|
||
|
case protoreflect.Sfixed64Kind:
|
||
|
return protoreflect.ValueOfInt64(int64(v)), nil
|
||
|
default:
|
||
|
// Fall back to protoreflect.ValueOf, which may panic if an unexpected type is passed.
|
||
|
return protoreflect.ValueOf(v), nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func toProtoEnumValue(fieldDesc protoreflect.FieldDescriptor, patchValue any) (protoreflect.Value, error) {
|
||
|
// For enums, we accept the field number or a string representation of the name
|
||
|
// (both supported by protojson). EnumNumber is a type alias for int32, but we
|
||
|
// may be dealing with a 64-bit number in Go depending on how it was obtained.
|
||
|
var enumValue protoreflect.EnumValueDescriptor
|
||
|
switch val := patchValue.(type) {
|
||
|
case string:
|
||
|
enumValue = fieldDesc.Enum().Values().ByName(protoreflect.Name(val))
|
||
|
case int:
|
||
|
enumValue = fieldDesc.Enum().Values().ByNumber(protoreflect.EnumNumber(val))
|
||
|
case int32:
|
||
|
enumValue = fieldDesc.Enum().Values().ByNumber(protoreflect.EnumNumber(val))
|
||
|
case int64:
|
||
|
enumValue = fieldDesc.Enum().Values().ByNumber(protoreflect.EnumNumber(val))
|
||
|
case uint:
|
||
|
enumValue = fieldDesc.Enum().Values().ByNumber(protoreflect.EnumNumber(val))
|
||
|
case uint32:
|
||
|
enumValue = fieldDesc.Enum().Values().ByNumber(protoreflect.EnumNumber(val))
|
||
|
case uint64:
|
||
|
enumValue = fieldDesc.Enum().Values().ByNumber(protoreflect.EnumNumber(val))
|
||
|
}
|
||
|
if enumValue != nil {
|
||
|
return protoreflect.ValueOfEnum(enumValue.Number()), nil
|
||
|
}
|
||
|
return typeMismatchErr(fieldDesc, patchValue)
|
||
|
}
|
||
|
|
||
|
// toProtoWrapperValue converts possible runtime Go types (per https://protobuf.dev/programming-guides/proto3/#scalar)
|
||
|
// to the appropriate proto wrapper target type based on the name of the given FieldDescriptor.
|
||
|
//
|
||
|
// This function does not attempt to handle overflows, only type conversion.
|
||
|
func toProtoWrapperValue(fieldDesc protoreflect.FieldDescriptor, patchValue any) (protoreflect.Value, error) {
|
||
|
fullName := string(fieldDesc.Message().FullName())
|
||
|
if !strings.HasPrefix(fullName, "google.protobuf.") || !strings.HasSuffix(fullName, "Value") {
|
||
|
return unsupportedTargetTypeErr(fieldDesc)
|
||
|
}
|
||
|
|
||
|
switch val := patchValue.(type) {
|
||
|
case int:
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
case int32:
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
case int64:
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
case uint:
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
case uint32:
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
case uint64:
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
case float32:
|
||
|
switch fieldDesc.Message().FullName() {
|
||
|
case "google.protobuf.FloatValue":
|
||
|
v := wrapperspb.Float(val)
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
case "google.protobuf.DoubleValue":
|
||
|
v := wrapperspb.Double(float64(val))
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
default:
|
||
|
// Fall back to int wrapper, since we may actually be targeting this instead.
|
||
|
// Failure will result in a typical type mismatch error.
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
}
|
||
|
case float64:
|
||
|
switch fieldDesc.Message().FullName() {
|
||
|
case "google.protobuf.FloatValue":
|
||
|
v := wrapperspb.Float(float32(val))
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
case "google.protobuf.DoubleValue":
|
||
|
v := wrapperspb.Double(val)
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
default:
|
||
|
// Fall back to int wrapper, since we may actually be targeting this instead.
|
||
|
// Failure will result in a typical type mismatch error.
|
||
|
return toProtoIntWrapperValue(fieldDesc, val)
|
||
|
}
|
||
|
case bool:
|
||
|
switch fieldDesc.Message().FullName() {
|
||
|
case "google.protobuf.BoolValue":
|
||
|
v := wrapperspb.Bool(val)
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
}
|
||
|
case string:
|
||
|
switch fieldDesc.Message().FullName() {
|
||
|
case "google.protobuf.StringValue":
|
||
|
v := wrapperspb.String(val)
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
}
|
||
|
}
|
||
|
return typeMismatchErr(fieldDesc, patchValue)
|
||
|
}
|
||
|
|
||
|
func toProtoIntWrapperValue[V int | int32 | int64 | uint | uint32 | uint64 | float32 | float64](fieldDesc protoreflect.FieldDescriptor, v V) (protoreflect.Value, error) {
|
||
|
switch fieldDesc.Message().FullName() {
|
||
|
case "google.protobuf.UInt32Value":
|
||
|
v := wrapperspb.UInt32(uint32(v))
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
case "google.protobuf.UInt64Value":
|
||
|
v := wrapperspb.UInt64(uint64(v))
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
case "google.protobuf.Int32Value":
|
||
|
v := wrapperspb.Int32(int32(v))
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
case "google.protobuf.Int64Value":
|
||
|
v := wrapperspb.Int64(int64(v))
|
||
|
return protoreflect.ValueOf(v.ProtoReflect()), nil
|
||
|
default:
|
||
|
return typeMismatchErr(fieldDesc, v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func typeMismatchErr(fieldDesc protoreflect.FieldDescriptor, v interface{}) (protoreflect.Value, error) {
|
||
|
return protoreflect.Value{}, fmt.Errorf("patch value type %T could not be applied to target field type '%s'", v, fieldType(fieldDesc))
|
||
|
}
|
||
|
|
||
|
func unsupportedTargetTypeErr(fieldDesc protoreflect.FieldDescriptor) (protoreflect.Value, error) {
|
||
|
return protoreflect.Value{}, fmt.Errorf("unsupported target field type '%s'", fieldType(fieldDesc))
|
||
|
}
|
||
|
|
||
|
func fieldType(fieldDesc protoreflect.FieldDescriptor) string {
|
||
|
v := fieldDesc.Kind().String()
|
||
|
// Scalars have a useful Kind string, but complex fields should have their full name for clarity.
|
||
|
if fieldDesc.Kind() == protoreflect.MessageKind {
|
||
|
v = string(fieldDesc.Message().FullName())
|
||
|
}
|
||
|
if fieldDesc.IsList() {
|
||
|
v = "repeated " + v // Kind reflects the type of repeated elements in repeated fields.
|
||
|
}
|
||
|
if fieldDesc.IsMap() {
|
||
|
v = "map" // maps are difficult to get types from, but we don't support them, so return a simple value for now.
|
||
|
}
|
||
|
return v
|
||
|
}
|