Skip to content

Commit

Permalink
Merge pull request #81 from alexejk/empty-values
Browse files Browse the repository at this point in the history
  • Loading branch information
alexejk authored Feb 17, 2024
2 parents 8c0fd9a + 6fd64e7 commit 7e4e933
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 51 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.5.1

Bugfixes:
* Handling of empty values while decoding responses (#80).
Library will now properly handle empty values for `<string>`, `<int>`, `<i4>`, `<boolean>`, `<double>`, `<dateTime.iso8601>`, `<base64>` and `<array>` (with case of `<data />`).
As `<struct>` may not have an empty list of `<member>` elements as per specification. Similarly `<array/>` is considered invalid.

## 0.5.0

Improvements:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21-alpine
FROM golang:1.22-alpine

# Build dependencies
RUN apk --no-cache update
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ Response is decoded following similar rules to argument encoding.
* 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.

#### Handling of Empty Values

If XML-RPC response contains no value for well-known data-types, it will be decoded into the default "empty" values as per table below:

| XML-RPC Value | Default Value |
|-------------------------|--------------|
| `<string/>` | `""` |
| `<int/>`, `<i4/>` | `0` |
| `<boolean/>` | `false` |
| `<double/>` | `0.0` |
| `<dateTime.iso8601/>` | `time.Time{}` |
| `<base64/>` | `nil` |
| `<array><data/><array>` | `nil` |

As per XML-RPC specification, `<struct>` may not have an empty list of `<member>` elements, thus no default "empty" value is defined for it.
Similarly, `<array/>` is considered invalid.

### Field renaming

XML-RPC specification does not necessarily specify any rules for struct's member names. Some services allow struct member names to include characters not compatible with standard Go field naming.
Expand Down
2 changes: 1 addition & 1 deletion client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestClient_Call(t *testing.T) {
require.Equal(t, 2, len(nameParts))
require.Equal(t, "my", nameParts[0], "test server: method should start with 'my.'")
require.Equal(t, 1, len(m.Params))
require.Equal(t, "12345", m.Params[0].Value.Int)
require.Equal(t, "12345", *m.Params[0].Value.Int)

file := nameParts[1]
_, _ = fmt.Fprintln(w, string(loadTestFile(t, fmt.Sprintf("response_%s.xml", file))))
Expand Down
76 changes: 53 additions & 23 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ func (d *StdDecoder) decodeFault(fault *ResponseFault) *Fault {
for _, m := range fault.Value.Struct {
switch m.Name {
case "faultCode":
if m.Value.Int != "" {
f.Code, _ = strconv.Atoi(m.Value.Int)
if m.Value.Int != nil {
f.Code, _ = strconv.Atoi(*m.Value.Int)
} else if m.Value.Int4 != nil {
f.Code, _ = strconv.Atoi(*m.Value.Int4)
} else {
f.Code, _ = strconv.Atoi(m.Value.Int4)
f.Code = 0 // Unknown fault code
}
case "faultString":
f.String = m.Value.String
if m.Value.String != nil {
f.String = *m.Value.String
}
}
}

Expand All @@ -92,36 +96,41 @@ func (d *StdDecoder) decodeValue(value *ResponseValue, field reflect.Value) erro
var err error

switch {
case value.Int != "":
val, err = strconv.Atoi(value.Int)
case value.Int != nil:
val, err = d.decodeInt(*value.Int)

case value.Int4 != "":
val, err = strconv.Atoi(value.Int4)
case value.Int4 != nil:
val, err = d.decodeInt(*value.Int4)

case value.Double != "":
val, err = strconv.ParseFloat(value.Double, float64BitSize)
case value.Double != nil:
val, err = d.decodeDouble(*value.Double)

case value.Boolean != "":
val, err = d.decodeBoolean(value.Boolean)
case value.Boolean != nil:
val, err = d.decodeBoolean(*value.Boolean)

case value.String != "":
val, err = value.String, nil
case value.String != nil:
val, err = *value.String, nil

case value.Base64 != "":
val, err = d.decodeBase64(value.Base64)
case value.Base64 != nil:
val, err = d.decodeBase64(*value.Base64)

case value.DateTime != "":
val, err = d.decodeDateTime(value.DateTime)
case value.DateTime != nil:
val, err = d.decodeDateTime(*value.DateTime)

// Array decoding
case len(value.Array) > 0:

case value.Array != nil:
if field.Kind() != reflect.Slice {
return fmt.Errorf(errFormatInvalidFieldType, reflect.Slice.String(), field.Kind().String())
}

slice := reflect.MakeSlice(reflect.TypeOf(field.Interface()), len(value.Array), len(value.Array))
for i, v := range value.Array {
values := value.Array.Values
if len(values) == 0 {
val, err = nil, nil
break
}

slice := reflect.MakeSlice(reflect.TypeOf(field.Interface()), len(values), len(values))
for i, v := range values {
item := slice.Index(i)
if err := d.decodeValue(v, item); err != nil {
return fmt.Errorf("failed decoding array item at index %d: %w", i, err)
Expand All @@ -132,7 +141,6 @@ func (d *StdDecoder) decodeValue(value *ResponseValue, field reflect.Value) erro

// Struct decoding
case len(value.Struct) != 0:

fieldKind := field.Kind()
if fieldKind != reflect.Struct && fieldKind != reflect.Map {
return fmt.Errorf(errFormatInvalidFieldTypeOrType, reflect.Struct.String(), reflect.Map.String(), fieldKind.String())
Expand Down Expand Up @@ -203,22 +211,44 @@ func (d *StdDecoder) decodeValue(value *ResponseValue, field reflect.Value) erro
return nil
}

func (d *StdDecoder) decodeInt(value string) (int, error) {
if value == "" {
return 0, nil
}
return strconv.Atoi(value)
}

func (d *StdDecoder) decodeDouble(value string) (float64, error) {
if value == "" {
return 0.0, nil
}
return strconv.ParseFloat(value, float64BitSize)
}

func (d *StdDecoder) decodeBoolean(value string) (bool, error) {
switch value {
case "1", "true", "TRUE", "True":
return true, nil
case "0", "false", "FALSE", "False":
return false, nil
case "":
return false, nil
default:
return false, fmt.Errorf("unrecognized value '%s' for boolean", value)
}
}

func (d *StdDecoder) decodeBase64(value string) ([]byte, error) {
if value == "" {
return nil, nil
}
return base64.StdEncoding.DecodeString(value)
}

func (d *StdDecoder) decodeDateTime(value string) (time.Time, error) {
if value == "" {
return time.Time{}, nil
}
return time.Parse(time.RFC3339, value)
}

Expand Down
21 changes: 13 additions & 8 deletions decode_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ type ResponseParam struct {
// ResponseValue encapsulates one of the data types for each parameter.
// Only one field should be set.
type ResponseValue struct {
Array []*ResponseValue `xml:"array>data>value"`
Array *ResponseArrayData `xml:"array>data"`
Struct []*ResponseStructMember `xml:"struct>member"`
String string `xml:"string"`
Int string `xml:"int"`
Int4 string `xml:"i4"`
Double string `xml:"double"`
Boolean string `xml:"boolean"`
DateTime string `xml:"dateTime.iso8601"`
Base64 string `xml:"base64"`
String *string `xml:"string"`
Int *string `xml:"int"`
Int4 *string `xml:"i4"`
Double *string `xml:"double"`
Boolean *string `xml:"boolean"`
DateTime *string `xml:"dateTime.iso8601"`
Base64 *string `xml:"base64"`

RawXML string `xml:",innerxml"`
}
Expand All @@ -47,6 +47,11 @@ type ResponseStructMember struct {
Value ResponseValue `xml:"value"`
}

// ResponseArrayData contains a list of array values
type ResponseArrayData struct {
Values []*ResponseValue `xml:"value"`
}

// ResponseFault wraps around failure
type ResponseFault struct {
Value ResponseValue `xml:"value"`
Expand Down
Loading

0 comments on commit 7e4e933

Please sign in to comment.