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

Handling of empty values #81

Merged
merged 4 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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)

Check warning on line 78 in decode.go

View check run for this annotation

Codecov / codecov/patch

decode.go#L78

Added line #L78 was not covered by tests
} else {
f.Code, _ = strconv.Atoi(m.Value.Int4)
f.Code = 0 // Unknown fault code

Check warning on line 80 in decode.go

View check run for this annotation

Codecov / codecov/patch

decode.go#L80

Added line #L80 was not covered by tests
}
case "faultString":
f.String = m.Value.String
if m.Value.String != nil {
f.String = *m.Value.String
}
}
}

Expand All @@ -92,36 +96,41 @@
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 @@

// 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 @@
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)

Check warning on line 225 in decode.go

View check run for this annotation

Codecov / codecov/patch

decode.go#L225

Added line #L225 was not covered by tests
}

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
Loading