Skip to content

Commit

Permalink
feat: add EchoErrorDetails RPC for Any testing (#1385)
Browse files Browse the repository at this point in the history
  • Loading branch information
vchudnov-g committed Nov 8, 2023
1 parent 7fee7c0 commit c839a8e
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 1 deletion.
50 changes: 49 additions & 1 deletion schema/google/showcase/v1beta1/echo.proto
Expand Up @@ -19,6 +19,7 @@ import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/routing.proto";
import "google/longrunning/operations.proto";
import "google/protobuf/any.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
import "google/rpc/status.proto";
Expand All @@ -34,7 +35,7 @@ option ruby_package = "Google::Showcase::V1beta1";
// side streaming, client side streaming, and bidirectional streaming. This
// service also exposes methods that explicitly implement server delay, and
// paginated calls. Set the 'showcase-trailer' metadata key on any method
// to have the values echoed in the response trailers. Set the
// to have the values echoed in the response trailers. Set the
// 'x-goog-request-params' metadata key on any method to have the values
// echoed in the response headers.
service Echo {
Expand Down Expand Up @@ -83,6 +84,19 @@ service Echo {
};
}

// This method returns error details in a repeated "google.protobuf.Any"
// field. This method showcases handling errors thus encoded, particularly
// over REST transport. Note that GAPICs only allow the type
// "google.protobuf.Any" for field paths ending in "error.details", and, at
// run-time, the actual types for these fields must be one of the types in
// google/rpc/error_details.proto.
rpc EchoErrorDetails(EchoErrorDetailsRequest) returns (EchoErrorDetailsResponse) {
option (google.api.http) = {
post: "/v1beta1/echo:error-details"
body: "*"
};
}

// This method splits the given content into words and will pass each word back
// through the stream. This method showcases server-side streaming RPCs.
rpc Expand(ExpandRequest) returns (stream EchoResponse) {
Expand Down Expand Up @@ -206,6 +220,40 @@ message EchoResponse {
Severity severity = 2;
}

// The request message used for the EchoErrorDetails method.
message EchoErrorDetailsRequest {
// Content to return in a singular `*.error.details` field of type
// `google.protobuf.Any`
string single_detail_text = 1;

// Content to return in a repeated `*.error.details` field of type
// `google.protobuf.Any`
repeated string multi_detail_text = 2;
}

// The response message used for the EchoErrorDetails method.
message EchoErrorDetailsResponse {

message SingleDetail {
ErrorWithSingleDetail error = 1;
}

message MultipleDetails {
ErrorWithMultipleDetails error = 1;
}

SingleDetail single_detail = 1;
MultipleDetails multiple_details = 2;
}

message ErrorWithSingleDetail {
google.protobuf.Any details = 1;
}

message ErrorWithMultipleDetails {
repeated google.protobuf.Any details = 1;
}

// The request message for the Expand method.
message ExpandRequest {
// The content that will be split into words and returned on the stream.
Expand Down
47 changes: 47 additions & 0 deletions server/services/echo_service.go
Expand Up @@ -16,6 +16,7 @@ package services

import (
"context"
"fmt"
"io"
"strconv"
"strings"
Expand All @@ -26,10 +27,12 @@ import (
"github.com/googleapis/gapic-showcase/server"
pb "github.com/googleapis/gapic-showcase/server/genproto"
lropb "google.golang.org/genproto/googleapis/longrunning"
errdetails "google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
anypb "google.golang.org/protobuf/types/known/anypb"
)

// NewEchoServer returns a new EchoServer for the Showcase API.
Expand All @@ -51,6 +54,50 @@ func (s *echoServerImpl) Echo(ctx context.Context, in *pb.EchoRequest) (*pb.Echo
return &pb.EchoResponse{Content: in.GetContent(), Severity: in.GetSeverity()}, nil
}

func (s *echoServerImpl) EchoErrorDetails(ctx context.Context, in *pb.EchoErrorDetailsRequest) (*pb.EchoErrorDetailsResponse, error) {
var singleDetailError *pb.EchoErrorDetailsResponse_SingleDetail
singleDetailText := in.GetSingleDetailText()
if len(singleDetailText) > 0 {
singleErrorInfo := &errdetails.ErrorInfo{Reason: singleDetailText}
singleMarshalledError, err := anypb.New(singleErrorInfo)
if err != nil {
return nil, fmt.Errorf("failure with single error detail in EchoErrorDetails: %w", err)
}
singleDetailError = &pb.EchoErrorDetailsResponse_SingleDetail{
Error: &pb.ErrorWithSingleDetail{Details: singleMarshalledError},
}
}

var multipleDetailsError *pb.EchoErrorDetailsResponse_MultipleDetails
multipleDetailText := in.GetMultiDetailText()
if len(multipleDetailText) > 0 {
details := []*anypb.Any{}
for idx, text := range multipleDetailText {
errorInfo := &errdetails.ErrorInfo{
Reason: text,
}
marshalledError, err := anypb.New(errorInfo)
if err != nil {
return nil, fmt.Errorf("failure in EchoErrorDetails[%d]: %w", idx, err)
}

details = append(details, marshalledError)
}

multipleDetailsError = &pb.EchoErrorDetailsResponse_MultipleDetails{
Error: &pb.ErrorWithMultipleDetails{Details: details},
}
}

echoHeaders(ctx)
echoTrailers(ctx)
response := &pb.EchoErrorDetailsResponse{
SingleDetail: singleDetailError,
MultipleDetails: multipleDetailsError,
}
return response, nil
}

func (s *echoServerImpl) Expand(in *pb.ExpandRequest, stream pb.Echo_ExpandServer) error {
for _, word := range strings.Fields(in.GetContent()) {
err := stream.Send(&pb.EchoResponse{Content: word})
Expand Down
117 changes: 117 additions & 0 deletions server/services/echo_service_test.go
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/golang/protobuf/ptypes"
durpb "github.com/golang/protobuf/ptypes/duration"
pb "github.com/googleapis/gapic-showcase/server/genproto"
"google.golang.org/genproto/googleapis/rpc/errdetails"
spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -402,6 +403,122 @@ func TestChat(t *testing.T) {
}
}

func TestEchoErrorDetails_single(t *testing.T) {
tests := []struct {
text string
expected *errdetails.ErrorInfo
}{
{"Spanish rain", &errdetails.ErrorInfo{Reason: "Spanish rain"}},
{"", &errdetails.ErrorInfo{Reason: ""}},
}

server := NewEchoServer()
for idx, test := range tests {
request := &pb.EchoErrorDetailsRequest{SingleDetailText: test.text}
out, err := server.EchoErrorDetails(context.Background(), request)
if err != nil {
t.Errorf("[%d] error calling EchoErrorSingleDetail(): %v", idx, err)
continue
}
if out.MultipleDetails != nil {
t.Errorf("[%d] expected no MultipleDetails, but got: %#v", idx, out.MultipleDetails)
}
if len(test.text) == 0 {
if out.SingleDetail != nil {
t.Errorf("[%d] expected no SingleDetail, but got: %#v", idx, out.SingleDetail)
}
continue
}
if out.SingleDetail == nil {
t.Errorf("[%d] no SingleDetail returned", idx)
continue
}
if out.SingleDetail.Error == nil {
t.Errorf("[%d] no SingleDetail.Error returned", idx)
continue
}
if out.SingleDetail.Error.Details == nil {
t.Errorf("[%d] no SingleDetail.Error.Details returned", idx)
continue
}
if got, want := out.SingleDetail.Error.Details.TypeUrl, "type.googleapis.com/google.rpc.ErrorInfo"; got != want {
t.Errorf("[%d] expected type URL %q; got %q ", idx, want, got)
}
unmarshalledError := &errdetails.ErrorInfo{}
if err := out.SingleDetail.Error.Details.UnmarshalTo(unmarshalledError); err != nil {
t.Errorf("[%d] error unmarshalling to ErrorInfo: %v", idx, err)
}
if got, want := unmarshalledError, test.expected; !proto.Equal(got, want) {
t.Errorf("[%d] expected ErrorInfo %v; got %v ", idx, want, got)
}
}
}

func TestEchoErrorDetails_multiple(t *testing.T) {
tests := []struct {
text []string
expected []*errdetails.ErrorInfo
}{
{
[]string{"rain", "snow", "hail", "sleet", "fog"},
[]*errdetails.ErrorInfo{
{Reason: "rain"},
{Reason: "snow"},
{Reason: "hail"},
{Reason: "sleet"},
{Reason: "fog"},
},
},
{nil, nil},
}

server := NewEchoServer()
for idx, test := range tests {
request := &pb.EchoErrorDetailsRequest{MultiDetailText: test.text}
out, err := server.EchoErrorDetails(context.Background(), request)
if err != nil {
t.Errorf("[%d] error calling EchoErrorDetails(): %v", idx, err)
continue
}
if out.SingleDetail != nil {
t.Errorf("[%d] expected no SingleDetail, but got: %#v", idx, out.SingleDetail)
}
if len(test.text) == 0 {
if out.MultipleDetails != nil {
t.Errorf("[%d] expected no MultipleDetails, but got %#v", idx, out.MultipleDetails)
}
continue
}
if out.MultipleDetails == nil {
t.Errorf("[%d] no MultipleDetails returned", idx)
continue
}
if out.MultipleDetails.Error == nil {
t.Errorf("[%d] no MultipleDetails.Error returned", idx)
continue
}
if out.MultipleDetails.Error.Details == nil {
t.Errorf("[%d] no MultipleDetails.Error.Details returned", idx)
continue
}
if got, want := len(out.MultipleDetails.Error.Details), len(test.expected); got != want {
t.Errorf("[%d] expected %d MultipleDetails.Error.Details, got %d", idx, want, got)
}
for whichDetail, detail := range out.MultipleDetails.Error.Details {
if got, want := detail.TypeUrl, "type.googleapis.com/google.rpc.ErrorInfo"; got != want {
t.Errorf("[%d:%d] expected type URL %q; got %q ", idx, whichDetail, want, got)
}
unmarshalledError := &errdetails.ErrorInfo{}
if err := detail.UnmarshalTo(unmarshalledError); err != nil {
t.Errorf("[%d:%d] error unmarshalling to ErrorInfo: %v", idx, whichDetail, err)
}
if got, want := unmarshalledError, test.expected[whichDetail]; !proto.Equal(got, want) {
t.Errorf("[%d:%d] expected ErrorInfo %v; got %v ", idx, whichDetail, want, got)
}
}
}
}

type errorChatStream struct {
err error
pb.Echo_ChatServer
Expand Down

0 comments on commit c839a8e

Please sign in to comment.