Skip to content

Commit

Permalink
Merge pull request #79 from alexejk/decode-struct-as-map
Browse files Browse the repository at this point in the history
  • Loading branch information
alexejk authored Feb 15, 2024
2 parents 2830010 + abbf585 commit 8c0fd9a
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 17 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.5.0

Improvements:

* Ability to decode struct members into a map.

## 0.4.1

Bugfixes:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Response is decoded following similar rules to argument encoding.
* Order of fields is important.
* Outer struct should contain exported field for each response parameter (it is possible to ignore unknown structs with `SkipUnknownFields` option).
* Structs may contain pointers - they will be initialized if required.
* Structs may be parsed as `map[string]any`, in case struct member names are not known at compile time. Map keys are enforced to `string` type.

### Field renaming

Expand Down
55 changes: 40 additions & 15 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
)

const (
errFormatInvalidFieldType = "invalid field type: expected '%s', got '%s'"
float64BitSize = 64
errFormatInvalidFieldType = "invalid field type: expected '%s', got '%s'"
errFormatInvalidFieldTypeOrType = "invalid field type: expected '%s' or '%s', got '%s'"
errFormatInvalidMapKeyTypeForStruct = "invalid map key type: must be 'string' when decoding structs into a map, got '%s'"
float64BitSize = 64
)

// Decoder implementations provide mechanisms for parsing of XML-RPC responses to native data-types.
Expand Down Expand Up @@ -131,24 +133,47 @@ func (d *StdDecoder) decodeValue(value *ResponseValue, field reflect.Value) erro
// Struct decoding
case len(value.Struct) != 0:

if field.Kind() != reflect.Struct {
return fmt.Errorf(errFormatInvalidFieldType, reflect.Struct.String(), field.Kind().String())
fieldKind := field.Kind()
if fieldKind != reflect.Struct && fieldKind != reflect.Map {
return fmt.Errorf(errFormatInvalidFieldTypeOrType, reflect.Struct.String(), reflect.Map.String(), fieldKind.String())
}

// If we are targeting a map, it should be initialized
if fieldKind == reflect.Map {
if kt := field.Type().Key().Kind(); kt != reflect.String {
return fmt.Errorf(errFormatInvalidMapKeyTypeForStruct, kt.String())
}

if field.IsNil() {
field.Set(reflect.MakeMap(field.Type()))
}
}

for _, m := range value.Struct {
// Upper-case the name
fName := structMemberToFieldName(m.Name)
f := findFieldByNameOrTag(field, fName)
if fieldKind == reflect.Map {
mapKey := reflect.ValueOf(m.Name)
f := reflect.New(field.Type().Elem()).Elem()

if !f.IsValid() {
if d.skipUnknownFields {
continue
if err := d.decodeValue(&m.Value, f); err != nil {
return fmt.Errorf("failed decoding struct member '%s': %w", m.Name, err)
}
return fmt.Errorf("cannot find field '%s' on struct", fName)
}

if err := d.decodeValue(&m.Value, f); err != nil {
return fmt.Errorf("failed decoding struct member '%s': %w", m.Name, err)
field.SetMapIndex(mapKey, f)
} else {
// Upper-case the name
fName := structMemberToFieldName(m.Name)
f := findFieldByNameOrTag(field, fName)

if !f.IsValid() {
if d.skipUnknownFields {
continue
}
return fmt.Errorf("cannot find field '%s' on struct", fName)
}

if err := d.decodeValue(&m.Value, f); err != nil {
return fmt.Errorf("failed decoding struct member '%s': %w", m.Name, err)
}
}
}

Expand Down Expand Up @@ -236,7 +261,7 @@ func getFieldNameFromTag(f *reflect.StructField, tagName string) string {

return keyName
}
if len(tagValue) > 0 && tagValue != "-" {
if tagValue != "" && tagValue != "-" {
keyName = tagValue
}

Expand Down
83 changes: 82 additions & 1 deletion decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func TestStdDecoder_DecodeRaw(t *testing.T) {
Struct string // <- This is unexpected type
}{},
expect: nil,
err: fmt.Errorf(errFormatInvalidFieldType, "struct", "string"),
err: fmt.Errorf(errFormatInvalidFieldTypeOrType, "struct", "map", "string"),
},
}

Expand Down Expand Up @@ -348,6 +348,87 @@ func TestStdDecoder_DecodeRaw_Arrays(t *testing.T) {
}
}

func TestStdDecoder_DecodeRaw_Struct_Map(t *testing.T) {
type DataType struct {
Id string `json:"id" xmlrpc:"id"`
PubDate string `json:"pub_date" xmlrpc:"pub_date"`
Title string `json:"title" xmlrpc:"title"`
}

type TestResponse struct {
Data map[string][][]DataType
}

tests := map[string]struct {
testFile string
v interface{}
expect interface{}
err error
}{
"Basic struct to map": {
testFile: "response_struct.xml",
v: &struct {
Data map[string]any
}{},
expect: &struct {
Data map[string]any
}{
Data: map[string]any{
"foo": "bar",
"baz": 2,
"woBleBobble": true,
"WoBleBobble2": 34,
"2": 3,
},
},
},
"Invalid key type": {
testFile: "response_struct.xml",
v: &struct {
Data map[any]any
}{},
err: fmt.Errorf(errFormatInvalidMapKeyTypeForStruct, "interface"),
},
"Nested structs to map": {
testFile: "response_nested_random_struct.xml",
v: &TestResponse{},
expect: &TestResponse{
Data: map[string][][]DataType{
"TESTING1": {
{
{Id: "1009470", PubDate: "2020-01-11 00:00:00", Title: "TITLE"},
{Id: "1009879", PubDate: "2020-01-11 00:00:00", Title: "TITLE2"},
{Id: "1304451", PubDate: "2020-01-13 17:16:49", Title: "Title3"},
},
},
"TESTING2": {
{
{Id: "1329812", PubDate: "2020-01-11 00:00:00", Title: "NewTitle"},
{Id: "1489372", PubDate: "2021-01-11 00:00:00", Title: "NextTitle"},
{Id: "1229276", PubDate: "2020-01-13 17:16:49", Title: "Title12"},
},
},
},
},
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
dec := &StdDecoder{}
decodeTarget := tt.v
err := dec.DecodeRaw(loadTestFile(t, tt.testFile), decodeTarget)
if tt.err == nil {
require.NoError(t, err)
require.EqualValues(t, tt.expect, decodeTarget)
} else {
require.Error(t, err)
require.Equal(t, tt.err, err)
}
})
}
}

func Test_fieldsMustEqual(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion hack/linter.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ GREEN="\033[32m"
YELLOW="\033[33m"
NORMAL="\033[39m"

LINTER_VERSION=1.55.2
LINTER_VERSION=1.56.1

LINTER_BINDIR=$(go env GOPATH)/bin
LINTER_NAME=golangci-lint
Expand Down
65 changes: 65 additions & 0 deletions testdata/response_nested_random_struct.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<name>TESTING1</name>
<value>
<array>
<data>
<value>
<array>
<data>
<value>
<struct>
<member><name>id</name><value><string>1009470</string></value></member>
<member><name>pub_date</name><value><string>2020-01-11 00:00:00</string></value></member>
<member><name>title</name><value><string>TITLE</string></value></member>
</struct></value>
<value><struct>
<member><name>title</name><value><string>TITLE2</string></value></member>
<member><name>pub_date</name><value><string>2020-01-11 00:00:00</string></value></member>
<member><name>id</name><value><string>1009879</string></value></member>
</struct></value>
<value><struct>
<member><name>title</name><value><string>Title3</string></value></member>
<member><name>pub_date</name><value><string>2020-01-13 17:16:49</string></value></member>
<member><name>id</name><value><string>1304451</string></value></member>
</struct></value>
</data></array></value>
</data></array></value>
</member>
<member>
<name>TESTING2</name>
<value>
<array>
<data>
<value>
<array>
<data>
<value>
<struct>
<member><name>id</name><value><string>1329812</string></value></member>
<member><name>pub_date</name><value><string>2020-01-11 00:00:00</string></value></member>
<member><name>title</name><value><string>NewTitle</string></value></member>
</struct></value>
<value><struct>
<member><name>title</name><value><string>NextTitle</string></value></member>
<member><name>pub_date</name><value><string>2021-01-11 00:00:00</string></value></member>
<member><name>id</name><value><string>1489372</string></value></member>
</struct></value>
<value><struct>
<member><name>title</name><value><string>Title12</string></value></member>
<member><name>pub_date</name><value><string>2020-01-13 17:16:49</string></value></member>
<member><name>id</name><value><string>1229276</string></value></member>
</struct></value>
</data></array></value>
</data></array></value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>

0 comments on commit 8c0fd9a

Please sign in to comment.