Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom post decoder #284

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions mapstructure.go
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 != "" {
Expand Down
129 changes: 129 additions & 0 deletions mapstructure_test.go
Expand Up @@ -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)
Expand Down