status: Add WithDetails and Details functions (#1358)

This commit is contained in:
Kaveh Ghasemloo 2017-07-24 17:42:24 -04:00 committed by dfawley
parent 6495e8dfeb
commit cdc12d4a3c
2 changed files with 185 additions and 0 deletions

View File

@ -28,9 +28,11 @@
package status
import (
"errors"
"fmt"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
)
@ -128,3 +130,39 @@ func FromError(err error) (s *Status, ok bool) {
}
return nil, false
}
// WithDetails returns a new status with the provided details messages appended to the status.
// If any errors are encountered, it returns nil and the first error encountered.
func (s *Status) WithDetails(details ...proto.Message) (*Status, error) {
if s.Code() == codes.OK {
return nil, errors.New("no error details for status with code OK")
}
// s.Code() != OK implies that s.Proto() != nil.
p := s.Proto()
for _, detail := range details {
any, err := ptypes.MarshalAny(detail)
if err != nil {
return nil, err
}
p.Details = append(p.Details, any)
}
return &Status{s: p}, nil
}
// Details returns a slice of details messages attached to the status.
// If a detail cannot be decoded, the error is returned in place of the detail.
func (s *Status) Details() []interface{} {
if s == nil || s.s == nil {
return nil
}
details := make([]interface{}, 0, len(s.s.Details))
for _, any := range s.s.Details {
detail := &ptypes.DynamicAny{}
if err := ptypes.UnmarshalAny(any, detail); err != nil {
details = append(details, err)
continue
}
details = append(details, detail.Message)
}
return details
}

View File

@ -19,11 +19,17 @@
package status
import (
"errors"
"fmt"
"reflect"
"testing"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
apb "github.com/golang/protobuf/ptypes/any"
dpb "github.com/golang/protobuf/ptypes/duration"
cpb "google.golang.org/genproto/googleapis/rpc/code"
epb "google.golang.org/genproto/googleapis/rpc/errdetails"
spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
)
@ -112,3 +118,144 @@ func TestFromErrorOK(t *testing.T) {
t.Fatalf("FromError(nil) = %v, %v; want <Code()=%s, Message()=%q, Err=nil>, true", s, ok, code, message)
}
}
func TestStatus_ErrorDetails(t *testing.T) {
tests := []struct {
code codes.Code
details []proto.Message
}{
{
code: codes.NotFound,
details: nil,
},
{
code: codes.NotFound,
details: []proto.Message{
&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
},
},
},
{
code: codes.Internal,
details: []proto.Message{
&epb.DebugInfo{
StackEntries: []string{
"first stack",
"second stack",
},
},
},
},
{
code: codes.Unavailable,
details: []proto.Message{
&epb.RetryInfo{
RetryDelay: &dpb.Duration{Seconds: 60},
},
&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
},
},
},
}
for _, tc := range tests {
s, err := New(tc.code, "").WithDetails(tc.details...)
if err != nil {
t.Fatalf("(%v).WithDetails(%+v) failed: %v", str(s), tc.details, err)
}
details := s.Details()
for i := range details {
if !proto.Equal(details[i].(proto.Message), tc.details[i]) {
t.Fatalf("(%v).Details()[%d] = %+v, want %+v", str(s), i, details[i], tc.details[i])
}
}
}
}
func TestStatus_WithDetails_Fail(t *testing.T) {
tests := []*Status{
nil,
FromProto(nil),
New(codes.OK, ""),
}
for _, s := range tests {
if s, err := s.WithDetails(); err == nil || s != nil {
t.Fatalf("(%v).WithDetails(%+v) = %v, %v; want nil, non-nil", str(s), []proto.Message{}, s, err)
}
}
}
func TestStatus_ErrorDetails_Fail(t *testing.T) {
tests := []struct {
s *Status
i []interface{}
}{
{
nil,
nil,
},
{
FromProto(nil),
nil,
},
{
New(codes.OK, ""),
[]interface{}{},
},
{
FromProto(&spb.Status{
Code: int32(cpb.Code_CANCELLED),
Details: []*apb.Any{
{
TypeUrl: "",
Value: []byte{},
},
mustMarshalAny(&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
}),
},
}),
[]interface{}{
errors.New(`message type url "" is invalid`),
&epb.ResourceInfo{
ResourceType: "book",
ResourceName: "projects/1234/books/5678",
Owner: "User",
},
},
},
}
for _, tc := range tests {
got := tc.s.Details()
if !reflect.DeepEqual(got, tc.i) {
t.Errorf("(%v).Details() = %+v, want %+v", str(tc.s), got, tc.i)
}
}
}
func str(s *Status) string {
if s == nil {
return "nil"
}
if s.s == nil {
return "<Code=OK>"
}
return fmt.Sprintf("<Code=%v, Message=%q, Details=%+v>", codes.Code(s.s.GetCode()), s.s.GetMessage(), s.s.GetDetails())
}
// mustMarshalAny converts a protobuf message to an any.
func mustMarshalAny(msg proto.Message) *apb.Any {
any, err := ptypes.MarshalAny(msg)
if err != nil {
panic(fmt.Sprintf("ptypes.MarshalAny(%+v) failed: %v", msg, err))
}
return any
}