From 0f70e921232c6e664ac658c5e314a8e6bf4b8414 Mon Sep 17 00:00:00 2001 From: zdunecki Date: Thu, 5 May 2022 08:06:38 +0200 Subject: [PATCH] feat(custom-decoder-post-decode): impl --- mapstructure.go | 19 +++++++ mapstructure_test.go | 129 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/mapstructure.go b/mapstructure.go index 7581806a..839c84cd 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -273,6 +273,10 @@ type DecoderConfig struct { // field name or tag. Defaults to `strings.EqualFold`. This can be used // to implement case-sensitive tag values, support snake casing, etc. MatchName func(mapKey, fieldName string) bool + + // If CustomPostDecoder is true, then after standard decoding + // for types that implements CustomDecoder the CustomDecoder.PostDecode function is run. + CustomPostDecoder bool } // A Decoder takes a raw interface value and turns it into structured @@ -285,6 +289,15 @@ type Decoder struct { config *DecoderConfig } +// CustomDecoder is the interface implemented by types that +// can decode themselves. +// This interface does something similar to json Marshaler . +// +// PostDecode is run after main decode implementation for each type. +type CustomDecoder interface { + PostDecode() error +} + // Metadata contains information about decoding a structure that // is tedious or difficult to get otherwise. type Metadata struct { @@ -495,6 +508,12 @@ func (d *Decoder) decode(name string, input interface{}, outVal reflect.Value) e return fmt.Errorf("%s: unsupported type: %s", name, outputKind) } + if d.config.CustomPostDecoder { + if m, ok := outVal.Interface().(CustomDecoder); ok && m != nil { + err = m.PostDecode() + } + } + // If we reached here, then we successfully decoded SOMETHING, so // mark the key as used if we're tracking metainput. if addMetaKey && d.config.Metadata != nil && name != "" { diff --git a/mapstructure_test.go b/mapstructure_test.go index d31129d7..088ce12c 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -2732,6 +2732,135 @@ func TestDecoder_IgnoreUntaggedFields(t *testing.T) { } } +type CustomDecoder_PostDecode_Brief struct { + FullName string + PrimaryEmail string +} + +type CustomDecoder_PostDecode_Contact struct { + Name string `mapstructure:"name"` + LastName string `mapstructure:"last_name"` + Emails []string `mapstructure:"emails"` + Brief *CustomDecoder_PostDecode_Brief +} + +func (t *CustomDecoder_PostDecode_Contact) PostDecode() error { + primaryEmail := "" + + if t.Emails != nil && len(t.Emails) > 0 { + primaryEmail = t.Emails[0] + } + + t.Brief = &CustomDecoder_PostDecode_Brief{ + FullName: t.Name + " " + t.LastName, + PrimaryEmail: primaryEmail, + } + + return nil +} + +func TestDecoder_CustomDecoder_PostDecode(t *testing.T) { + t.Parallel() + + input := []map[string]interface{}{ + { + "name": "John", + "last_name": "Doe", + "emails": []string{"john.doe@gmail.com"}, + }, + } + + expected := []*CustomDecoder_PostDecode_Contact{ + { + Name: "John", + LastName: "Doe", + Emails: []string{"john.doe@gmail.com"}, + Brief: &CustomDecoder_PostDecode_Brief{ + FullName: "John Doe", + PrimaryEmail: "john.doe@gmail.com", + }, + }, + } + + var actual []*CustomDecoder_PostDecode_Contact + + config := &DecoderConfig{ + Result: &actual, + IgnoreUntaggedFields: true, + CustomPostDecoder: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) + } +} + +type CustomDecoder_PostDecode_PrimaryEmail string + +func (s CustomDecoder_PostDecode_PrimaryEmail) ptr() *CustomDecoder_PostDecode_PrimaryEmail { + return &s +} + +func (s *CustomDecoder_PostDecode_PrimaryEmail) PostDecode() error { + v := *s + + *s = "#1 " + v + + return nil +} + +func TestDecoder_CustomDecoder_PostDecodeC(t *testing.T) { + t.Parallel() + + input := []map[string]interface{}{ + { + "primary_email": "john.doe@gmail.com", + }, + } + + type Target struct { + PrimaryEmail *CustomDecoder_PostDecode_PrimaryEmail `mapstructure:"primary_email"` + } + + expected := []*Target{ + { + PrimaryEmail: CustomDecoder_PostDecode_PrimaryEmail("#1 john.doe@gmail.com").ptr(), + }, + } + + var actual []*Target + + config := &DecoderConfig{ + Result: &actual, + IgnoreUntaggedFields: true, + CustomPostDecoder: true, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) + } +} + func testSliceInput(t *testing.T, input map[string]interface{}, expected *Slice) { var result Slice err := Decode(input, &result)