[#83] client: Implement status library

Define base `Status` interface. Provide the functionality to distinguish
success and failure returns. Provide functionality to transport statuses
over NeoFS API V2 protocol. Support success `OK` and failure `INTERNAL`
returns.

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2021-11-16 21:15:56 +03:00 committed by LeL
parent fc18ca2cb3
commit 9dcff95a29
20 changed files with 465 additions and 11 deletions

View file

@ -4,9 +4,9 @@ import (
"context"
"fmt"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/accounting"
"github.com/nspcc-dev/neofs-sdk-go/owner"

View file

@ -3,7 +3,7 @@ package client
import (
"io"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
)
// Client represents NeoFS client.

View file

@ -5,10 +5,10 @@ import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
v2container "github.com/nspcc-dev/neofs-api-go/v2/container"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/container"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"

View file

@ -4,9 +4,9 @@ import (
"context"
"fmt"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/netmap"
"github.com/nspcc-dev/neofs-sdk-go/version"

View file

@ -9,10 +9,10 @@ import (
"fmt"
"io"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
v2object "github.com/nspcc-dev/neofs-api-go/v2/object"
v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-api-go/v2/signature"
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"

View file

@ -5,8 +5,8 @@ import (
"crypto/tls"
"time"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-sdk-go/session"
"github.com/nspcc-dev/neofs-sdk-go/token"

View file

@ -3,7 +3,7 @@ package client
import (
"io"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
)
// Raw returns underlying raw protobuf client.

View file

@ -4,9 +4,9 @@ import (
"context"
"fmt"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/reputation"
)

View file

@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"github.com/nspcc-dev/neofs-api-go/rpc/client"
rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc"
"github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
v2session "github.com/nspcc-dev/neofs-api-go/v2/session"
v2signature "github.com/nspcc-dev/neofs-api-go/v2/signature"
"github.com/nspcc-dev/neofs-sdk-go/owner"

55
client/status/common.go Normal file
View file

@ -0,0 +1,55 @@
package apistatus
import (
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// ServerInternal describes failure statuses related to internal server errors.
// Instances provide Status and StatusV2 interfaces.
//
// The status is purely informative, the client should not go into details of the error except for debugging needs.
type ServerInternal struct {
v2 status.Status
}
func (x ServerInternal) Error() string {
return errMessageStatusV2(
globalizeCodeV2(status.Internal, status.GlobalizeCommonFail),
x.v2.Message(),
)
}
// implements method of the FromStatusV2 local interface.
func (x *ServerInternal) fromStatusV2(st *status.Status) {
x.v2 = *st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// * code: INTERNAL;
// * string message: empty;
// * details: empty.
func (x ServerInternal) ToStatusV2() *status.Status {
x.v2.SetCode(globalizeCodeV2(status.Internal, status.GlobalizeCommonFail))
return &x.v2
}
// SetMessage sets message describing internal error.
//
// Message should be used for debug purposes only.
func (x *ServerInternal) SetMessage(msg string) {
x.v2.SetMessage(msg)
}
// Message returns message describing internal server error.
//
// Message should be used for debug purposes only. By default, it is empty.
func (x ServerInternal) Message() string {
return x.v2.Message()
}
// WriteInternalServerErr writes err message to ServerInternal instance.
func WriteInternalServerErr(x *ServerInternal, err error) {
x.SetMessage(err.Error())
}

View file

@ -0,0 +1,26 @@
package apistatus_test
import (
"testing"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
func TestServerInternal_Message(t *testing.T) {
const msg = "some message"
var st apistatus.ServerInternal
res := st.Message()
resv2 := apistatus.ToStatusV2(st).Message()
require.Empty(t, res)
require.Empty(t, resv2)
st.SetMessage(msg)
res = st.Message()
resv2 = apistatus.ToStatusV2(st).Message()
require.Equal(t, msg, res)
require.Equal(t, msg, resv2)
}

44
client/status/status.go Normal file
View file

@ -0,0 +1,44 @@
package apistatus
// Status defines a variety of NeoFS API status returns.
//
// All statuses are split into two disjoint subsets: successful and failed, and:
// * statuses that implement the build-in error interface are considered failed statuses;
// * all other value types are considered successes (nil is a default success).
//
// In Go code type of success can be determined by a type switch, failure - by a switch with errors.As calls.
// Nil should be considered as a success, and default switch section - as an unrecognized Status.
//
// To convert statuses into errors and vice versa, use functions ErrToStatus and ErrFromStatus, respectively.
// ErrFromStatus function returns nil for successful statuses. However, to simplify the check of statuses for success,
// IsSuccessful function should be used (try to avoid nil comparison).
// It should be noted that using direct typecasting is not a compatible approach.
//
// To transport statuses using the NeoFS API V2 protocol, see StatusV2 interface and FromStatusV2 and ToStatusV2 functions.
type Status interface{}
// ErrFromStatus converts Status instance to error if it is failed. Returns nil on successful Status.
//
// Note: direct assignment may not be compatibility-safe.
func ErrFromStatus(st Status) error {
if err, ok := st.(error); ok {
return err
}
return nil
}
// ErrToStatus converts the error instance to Status instance.
//
// Note: direct assignment may not be compatibility-safe.
func ErrToStatus(err error) Status {
return err
}
// IsSuccessful checks if status is successful.
//
// Note: direct cast may not be compatibility-safe.
func IsSuccessful(st Status) bool {
_, ok := st.(error)
return !ok
}

View file

@ -0,0 +1,35 @@
package apistatus_test
import (
"errors"
"testing"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
func TestErrors(t *testing.T) {
t.Run("error source", func(t *testing.T) {
err := errors.New("some error")
st := apistatus.ErrToStatus(err)
success := apistatus.IsSuccessful(st)
require.False(t, success)
res := apistatus.ErrFromStatus(st)
require.Equal(t, err, res)
})
t.Run("non-error source", func(t *testing.T) {
var st apistatus.Status = "any non-error type"
success := apistatus.IsSuccessful(st)
require.True(t, success)
res := apistatus.ErrFromStatus(st)
require.Nil(t, res)
})
}

32
client/status/success.go Normal file
View file

@ -0,0 +1,32 @@
package apistatus
import (
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// SuccessDefaultV2 represents Status instance of default success. Implements StatusV2.
type SuccessDefaultV2 struct {
isNil bool
v2 *status.Status
}
// implements method of the FromStatusV2 local interface.
func (x *SuccessDefaultV2) fromStatusV2(st *status.Status) {
x.isNil = st == nil
x.v2 = st
}
// ToStatusV2 implements StatusV2 interface method.
// If the value was returned by FromStatusV2, returns the source message.
// Otherwise, returns message with
// * code: OK;
// * string message: empty;
// * details: empty.
func (x SuccessDefaultV2) ToStatusV2() *status.Status {
if x.isNil || x.v2 != nil {
return x.v2
}
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
}

View file

@ -0,0 +1,18 @@
package apistatus
import (
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
type unrecognizedStatusV2 struct {
v2 status.Status
}
func (x unrecognizedStatusV2) Error() string {
return errMessageStatusV2("unrecognized", x.v2.Message())
}
// implements method of the FromStatusV2 local interface.
func (x *unrecognizedStatusV2) fromStatusV2(st *status.Status) {
x.v2 = *st
}

99
client/status/v2.go Normal file
View file

@ -0,0 +1,99 @@
package apistatus
import (
"fmt"
"github.com/nspcc-dev/neofs-api-go/v2/status"
)
// StatusV2 defines a variety of Status instances compatible with NeoFS API V2 protocol.
//
// Note: it is not recommended to use this type directly, it is intended for documentation of the library functionality.
type StatusV2 interface {
Status
// ToStatusV2 returns the status as github.com/nspcc-dev/neofs-api-go/v2/status.Status message structure.
ToStatusV2() *status.Status
}
// FromStatusV2 converts status.Status message structure to Status instance. Inverse to ToStatusV2 operation.
//
// If result is not nil, it implements StatusV2. This fact should be taken into account only when passing
// the result to the inverse function ToStatusV2, casts are not compatibility-safe.
//
// Below is the mapping of return codes to Status instance types (with a description of parsing details).
// Note: notice if the return type is a pointer.
//
// Successes:
// * status.OK: *SuccessDefaultV2 (this also includes nil argument).
//
// Common failures:
// * status.Internal: *ServerInternal.
func FromStatusV2(st *status.Status) Status {
var decoder interface {
fromStatusV2(*status.Status)
}
switch code := st.Code(); {
case status.IsSuccess(code):
switch status.LocalizeSuccess(&code); code {
case status.OK:
decoder = new(SuccessDefaultV2)
}
case status.IsCommonFail(code):
switch status.LocalizeCommonFail(&code); code {
case status.Internal:
decoder = new(ServerInternal)
}
}
if decoder == nil {
decoder = new(unrecognizedStatusV2)
}
decoder.fromStatusV2(st)
return decoder
}
// ToStatusV2 converts Status instance to status.Status message structure. Inverse to FromStatusV2 operation.
//
// If argument is the StatusV2 instance, it is converted directly.
// Otherwise, successes are converted with status.OK code w/o details and message, failures - with status.Internal.
func ToStatusV2(st Status) *status.Status {
if v, ok := st.(StatusV2); ok {
return v.ToStatusV2()
}
if IsSuccessful(st) {
return newStatusV2WithLocalCode(status.OK, status.GlobalizeSuccess)
}
return newStatusV2WithLocalCode(status.Internal, status.GlobalizeCommonFail)
}
func errMessageStatusV2(code interface{}, msg string) string {
const (
noMsgFmt = "status: code = %v"
msgFmt = noMsgFmt + " message = %s"
)
if msg != "" {
return fmt.Sprintf(msgFmt, code, msg)
}
return fmt.Sprintf(noMsgFmt, code)
}
func newStatusV2WithLocalCode(code status.Code, globalizer func(*status.Code)) *status.Status {
var st status.Status
st.SetCode(globalizeCodeV2(code, globalizer))
return &st
}
func globalizeCodeV2(code status.Code, globalizer func(*status.Code)) status.Code {
globalizer(&code)
return code
}

145
client/status/v2_test.go Normal file
View file

@ -0,0 +1,145 @@
package apistatus_test
import (
"errors"
"testing"
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
"github.com/stretchr/testify/require"
)
func TestToStatusV2(t *testing.T) {
type statusConstructor func() apistatus.Status
for _, testItem := range [...]struct {
status interface{} // Status or statusConstructor
codeV2 uint64
}{
{
status: errors.New("some error"),
codeV2: 1024,
},
{
status: 1,
codeV2: 0,
},
{
status: "text",
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: nil,
codeV2: 0,
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.ServerInternal
st.SetMessage("internal error message")
return st
}),
codeV2: 1024,
},
} {
var st apistatus.Status
if cons, ok := testItem.status.(statusConstructor); ok {
st = cons()
} else {
st = testItem.status
}
stv2 := apistatus.ToStatusV2(st)
// must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code())
_, ok := st.(apistatus.StatusV2)
if ok {
// restore and convert again
restored := apistatus.FromStatusV2(stv2)
res := apistatus.ToStatusV2(restored)
// must generate the same status.Status message
require.Equal(t, stv2, res)
}
}
}
func TestFromStatusV2(t *testing.T) {
type statusConstructor func() apistatus.Status
for _, testItem := range [...]struct {
status interface{} // Status or statusConstructor
codeV2 uint64
}{
{
status: errors.New("some error"),
codeV2: 1024,
},
{
status: 1,
codeV2: 0,
},
{
status: "text",
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: true,
codeV2: 0,
},
{
status: nil,
codeV2: 0,
},
{
status: (statusConstructor)(func() apistatus.Status {
var st apistatus.ServerInternal
st.SetMessage("internal error message")
return st
}),
codeV2: 1024,
},
} {
var st apistatus.Status
if cons, ok := testItem.status.(statusConstructor); ok {
st = cons()
} else {
st = testItem.status
}
stv2 := apistatus.ToStatusV2(st)
// must generate the same status.Status message
require.EqualValues(t, testItem.codeV2, stv2.Code())
_, ok := st.(apistatus.StatusV2)
if ok {
// restore and convert again
restored := apistatus.FromStatusV2(stv2)
res := apistatus.ToStatusV2(restored)
// must generate the same status.Status message
require.Equal(t, stv2, res)
}
}
}

2
go.mod
View file

@ -9,7 +9,7 @@ require (
github.com/hashicorp/golang-lru v0.5.4
github.com/mr-tron/base58 v1.2.0
github.com/nspcc-dev/hrw v1.0.9
github.com/nspcc-dev/neofs-api-go v1.30.0
github.com/nspcc-dev/neofs-api-go/v2 v2.11.0-pre.0.20211118144033-580f6c5554ff
github.com/nspcc-dev/neofs-crypto v0.3.0
github.com/nspcc-dev/rfc6979 v0.2.0
github.com/stretchr/testify v1.7.0

BIN
go.sum

Binary file not shown.

View file

@ -10,7 +10,7 @@ import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
client "github.com/nspcc-dev/neofs-api-go/rpc/client"
client "github.com/nspcc-dev/neofs-api-go/v2/rpc/client"
accounting "github.com/nspcc-dev/neofs-sdk-go/accounting"
client0 "github.com/nspcc-dev/neofs-sdk-go/client"
container "github.com/nspcc-dev/neofs-sdk-go/container"