-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mapstructuredecoder that produces structpb safe output (#522)
- Loading branch information
1 parent
16d4d94
commit 0a5e5aa
Showing
6 changed files
with
164 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
changelog: | ||
- type: NON_USER_FACING | ||
description: > | ||
Normalizes and decodes map interfaces that can have (deeply nested) numbers of type json.Number | ||
by setting those to either to int64 or float64. | ||
Usually you don't need to normalize a map[string]interface{}, that you are decoding into a struct with mapstructure | ||
Unless, you will use it with `structpb.NewValue()` which doesn't handle that type and throws an invalid type error. | ||
issueLink: https://github.com/solo-io/gloo-mesh-enterprise/issues/12608 | ||
resolvesIssue: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package mapdecoder | ||
|
||
import ( | ||
"encoding/json" | ||
"reflect" | ||
|
||
"github.com/mitchellh/mapstructure" | ||
) | ||
|
||
// NormalizeMapDecode decodes a map[string]interface{} into the given result object | ||
// and handles converting json.Number to int64 or float64 which would cause errors in structpb.NewValue | ||
func NormalizeMapDecode(input interface{}, result interface{}) error { | ||
config := &mapstructure.DecoderConfig{ | ||
DecodeHook: mapstructure.ComposeDecodeHookFunc(jsonNumberToNumberHook()), | ||
Result: result, | ||
} | ||
|
||
decoder, err := mapstructure.NewDecoder(config) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return decoder.Decode(input) | ||
} | ||
|
||
// jsonNumberToNumberHook creates a DecodeHookFuncType that converts json.Number to int64 or float64 | ||
func jsonNumberToNumberHook() mapstructure.DecodeHookFuncType { | ||
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { | ||
if numberStr, ok := data.(json.Number); ok { | ||
if t.Kind() == reflect.Int64 { | ||
return numberStr.Int64() | ||
} | ||
|
||
if t.Kind() == reflect.Float64 { | ||
return numberStr.Float64() | ||
} | ||
} | ||
|
||
if f.Kind() == reflect.Map { | ||
// Recursively process the map | ||
return convertNumbersInMap(data) | ||
} | ||
|
||
return data, nil | ||
} | ||
} | ||
|
||
// convertNumbersInMap takes a map and converts all json.Number values to the appropriate numeric type | ||
func convertNumbersInMap(original interface{}) (interface{}, error) { | ||
resultMap := make(map[string]interface{}) | ||
for key, val := range original.(map[string]interface{}) { | ||
switch v := val.(type) { | ||
case json.Number: | ||
if intVal, err := v.Int64(); err == nil { | ||
resultMap[key] = intVal | ||
} else if floatVal, err := v.Float64(); err == nil { | ||
resultMap[key] = floatVal | ||
} else { | ||
// If it's not a number, just keep the original string | ||
resultMap[key] = val | ||
} | ||
case map[string]interface{}: | ||
// Recursively convert nested maps | ||
convertedMap, err := convertNumbersInMap(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
resultMap[key] = convertedMap | ||
default: | ||
resultMap[key] = val | ||
} | ||
} | ||
return resultMap, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package mapdecoder_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"go.uber.org/zap" | ||
|
||
"github.com/fgrosse/zaptest" | ||
"github.com/solo-io/go-utils/contextutils" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
"github.com/onsi/ginkgo/v2/reporters" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestMapDecoderServer(t *testing.T) { | ||
zaptest.Level = zap.InfoLevel | ||
logger := zaptest.LoggerWriter(GinkgoWriter) | ||
|
||
contextutils.SetFallbackLogger(logger.Sugar()) | ||
|
||
RegisterFailHandler(Fail) | ||
junitReporter := reporters.NewJUnitReporter("junit.xml") | ||
RunSpecsWithDefaultAndCustomReporters(t, "Map Decoder Suite", []Reporter{junitReporter}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package mapdecoder_test | ||
|
||
import ( | ||
"encoding/json" | ||
|
||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
. "github.com/solo-io/go-utils/mapstructureutils" | ||
"google.golang.org/protobuf/types/known/structpb" | ||
) | ||
|
||
var _ = Describe("Normalize Map Decode", func() { | ||
DescribeTable("should correctly decode JSON numbers into int64 or float64", | ||
func(input map[string]interface{}, expectedResult map[string]interface{}) { | ||
result := make(map[string]interface{}) | ||
err := NormalizeMapDecode(input, &result) | ||
Expect(err).NotTo(HaveOccurred()) | ||
|
||
Expect(result).To(Equal(expectedResult)) | ||
value, err := structpb.NewValue(result) | ||
Expect(err).NotTo(HaveOccurred()) | ||
Expect(value).ToNot(BeNil()) | ||
}, | ||
Entry("deeply nested JSON number to int", | ||
map[string]interface{}{ | ||
"number": json.Number("10"), | ||
"float": float64(10.5), | ||
"nested": map[string]interface{}{ | ||
"number": json.Number("100"), | ||
"float": float64(100.5), | ||
"deeplyNested": map[string]interface{}{ | ||
"number": json.Number("1000"), | ||
"float": float64(100.5), | ||
}, | ||
}, | ||
}, | ||
map[string]interface{}{ | ||
"number": int64(10), | ||
"float": float64(10.5), | ||
"nested": map[string]interface{}{ | ||
"number": int64(100), | ||
"float": float64(100.5), | ||
"deeplyNested": map[string]interface{}{ | ||
"number": int64(1000), | ||
"float": float64(100.5), | ||
}, | ||
}, | ||
}, | ||
), | ||
) | ||
}) |