Skip to content

Commit

Permalink
crud: support operation_data field in errors
Browse files Browse the repository at this point in the history
This patch adds `operation_data` decoding for the `crud.Error`.

The `operation_data` type is determined as `rowType` in `crud.Result`.

Also, according to [1], an error can contain one of the following:
- an error
- an array of errors
- nil

So the error decoding logic has been modified to consider each case,
in order to avoid comparing an error to nil.

1. https://github.com/tarantool/crud/tree/master#api

Closes #330
  • Loading branch information
askalt authored and oleg-jukovec committed Aug 23, 2023
1 parent b17735b commit d8df65d
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
- More linters on CI (#310)
- Meaningful description for read/write socket errors (#129)
- Support password and password file to decrypt private SSL key file (#319)
- Support `operation_data` in `crud.Error` (#330)

### Changed

Expand Down
36 changes: 32 additions & 4 deletions crud/error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package crud

import (
"reflect"
"strings"

"github.com/vmihailenco/msgpack/v5"
Expand All @@ -21,6 +22,15 @@ type Error struct {
Stack string
// Str is the text of reason with error class.
Str string
// OperationData is the object/tuple with which an error occurred.
OperationData interface{}
// operationDataType contains the type of OperationData.
operationDataType reflect.Type
}

// newError creates an Error object with a custom operation data type to decoding.
func newError(operationDataType reflect.Type) *Error {
return &Error{operationDataType: operationDataType}
}

// DecodeMsgpack provides custom msgpack decoder.
Expand Down Expand Up @@ -59,6 +69,18 @@ func (e *Error) DecodeMsgpack(d *msgpack.Decoder) error {
if e.Str, err = d.DecodeString(); err != nil {
return err
}
case "operation_data":
if e.operationDataType != nil {
tuple := reflect.New(e.operationDataType)
if err = d.DecodeValue(tuple); err != nil {
return err
}
e.OperationData = tuple.Elem().Interface()
} else {
if err = d.Decode(&e.OperationData); err != nil {
return err
}
}
default:
if err := d.Skip(); err != nil {
return err
Expand All @@ -77,6 +99,13 @@ func (e Error) Error() string {
// ErrorMany describes CRUD error object for `_many` methods.
type ErrorMany struct {
Errors []Error
// operationDataType contains the type of OperationData for each Error.
operationDataType reflect.Type
}

// newErrorMany creates an ErrorMany object with a custom operation data type to decoding.
func newErrorMany(operationDataType reflect.Type) *ErrorMany {
return &ErrorMany{operationDataType: operationDataType}
}

// DecodeMsgpack provides custom msgpack decoder.
Expand All @@ -88,16 +117,15 @@ func (e *ErrorMany) DecodeMsgpack(d *msgpack.Decoder) error {

var errs []Error
for i := 0; i < l; i++ {
var crudErr *Error = nil
crudErr := newError(e.operationDataType)
if err := d.Decode(&crudErr); err != nil {
return err
} else if crudErr != nil {
errs = append(errs, *crudErr)
}
errs = append(errs, *crudErr)
}

if len(errs) > 0 {
*e = ErrorMany{Errors: errs}
e.Errors = errs
}

return nil
Expand Down
86 changes: 86 additions & 0 deletions crud/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,92 @@ func ExampleResult_rowsCustomType() {
// [{{} 2010 45 bla}]
}

// ExampleResult_operationData demonstrates how to obtain information
// about erroneous objects from crud.Error using `OperationData` field.
func ExampleResult_operationData() {
conn := exampleConnect()
req := crud.MakeInsertObjectManyRequest(exampleSpace).Objects([]crud.Object{
crud.MapObject{
"id": 2,
"bucket_id": 3,
"name": "Makar",
},
crud.MapObject{
"id": 2,
"bucket_id": 3,
"name": "Vasya",
},
crud.MapObject{
"id": 3,
"bucket_id": 5,
},
})

ret := crud.Result{}
if err := conn.Do(req).GetTyped(&ret); err != nil {
crudErrs := err.(crud.ErrorMany)
fmt.Println("Erroneous data:")
for _, crudErr := range crudErrs.Errors {
fmt.Println(crudErr.OperationData)
}
} else {
fmt.Println(ret.Metadata)
fmt.Println(ret.Rows)
}

// Output:
// Erroneous data:
// [2 3 Vasya]
// map[bucket_id:5 id:3]
}

// ExampleResult_operationDataCustomType demonstrates the ability
// to cast `OperationData` field, extracted from a CRUD error during decoding
// using crud.Result, to a custom type.
// The type of `OperationData` is determined as the crud.Result row type.
func ExampleResult_operationDataCustomType() {
conn := exampleConnect()
req := crud.MakeInsertObjectManyRequest(exampleSpace).Objects([]crud.Object{
crud.MapObject{
"id": 1,
"bucket_id": 3,
"name": "Makar",
},
crud.MapObject{
"id": 1,
"bucket_id": 3,
"name": "Vasya",
},
crud.MapObject{
"id": 3,
"bucket_id": 5,
},
})

type Tuple struct {
Id uint64 `msgpack:"id,omitempty"`
BucketId uint64 `msgpack:"bucket_id,omitempty"`
Name string `msgpack:"name,omitempty"`
}

ret := crud.MakeResult(reflect.TypeOf(Tuple{}))
if err := conn.Do(req).GetTyped(&ret); err != nil {
crudErrs := err.(crud.ErrorMany)
fmt.Println("Erroneous data:")
for _, crudErr := range crudErrs.Errors {
operationData := crudErr.OperationData.(Tuple)
fmt.Println(operationData)
}
} else {
fmt.Println(ret.Metadata)
fmt.Println(ret.Rows)
}
// Output:
// Erroneous data:
// {1 3 Vasya}
// {3 5 }
}

// ExampleResult_many demonstrates that there is no difference in a
// response from *ManyRequest.
func ExampleResult_many() {
Expand Down
16 changes: 8 additions & 8 deletions crud/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,20 +137,20 @@ func (r *Result) DecodeMsgpack(d *msgpack.Decoder) error {

var retErr error
if msgpackIsArray(code) {
var crudErr *ErrorMany
crudErr := newErrorMany(r.rowType)
if err := d.Decode(&crudErr); err != nil {
return err
}
if crudErr != nil {
retErr = *crudErr
}
} else {
var crudErr *Error
retErr = *crudErr
} else if code != msgpcode.Nil {
crudErr := newError(r.rowType)
if err := d.Decode(&crudErr); err != nil {
return err
}
if crudErr != nil {
retErr = *crudErr
retErr = *crudErr
} else {
if err := d.DecodeNil(); err != nil {
return err
}
}

Expand Down
33 changes: 33 additions & 0 deletions crud/result_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package crud_test

import (
"bytes"
"reflect"
"testing"

"github.com/stretchr/testify/require"
"github.com/tarantool/go-tarantool/v2/crud"
"github.com/vmihailenco/msgpack/v5"
)

func TestResult_DecodeMsgpack(t *testing.T) {
sampleCrudResponse := []interface{}{
map[string]interface{}{
"rows": []interface{}{"1", "2", "3"},
},
nil,
}
responses := []interface{}{sampleCrudResponse, sampleCrudResponse}

b := bytes.NewBuffer([]byte{})
enc := msgpack.NewEncoder(b)
err := enc.Encode(responses)
require.NoError(t, err)

var results []crud.Result
decoder := msgpack.NewDecoder(b)
err = decoder.DecodeValue(reflect.ValueOf(&results))
require.NoError(t, err)
require.Equal(t, results[0].Rows, []interface{}{"1", "2", "3"})
require.Equal(t, results[1].Rows, []interface{}{"1", "2", "3"})
}

0 comments on commit d8df65d

Please sign in to comment.