Merge pull request #12632 from apolcyn/backport_google_rpc_status_in_ruby

Backport #12452 to 1.6.x
diff --git a/grpc.gemspec b/grpc.gemspec
index 9957f05..f495087 100644
--- a/grpc.gemspec
+++ b/grpc.gemspec
@@ -29,6 +29,7 @@
 
   s.add_dependency 'google-protobuf', '~> 3.1'
   s.add_dependency 'googleauth',      '~> 0.5.1'
+  s.add_dependency 'googleapis-common-protos-types', '~> 1.0.0'
 
   s.add_development_dependency 'bundler',            '~> 1.9'
   s.add_development_dependency 'facter',             '~> 2.4'
diff --git a/src/ruby/lib/grpc/google_rpc_status_utils.rb b/src/ruby/lib/grpc/google_rpc_status_utils.rb
new file mode 100644
index 0000000..fdadd6b
--- /dev/null
+++ b/src/ruby/lib/grpc/google_rpc_status_utils.rb
@@ -0,0 +1,28 @@
+# Copyright 2017 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative './grpc'
+require 'google/rpc/status_pb'
+
+# GRPC contains the General RPC module.
+module GRPC
+  # GoogleRpcStatusUtils provides utilities to convert between a
+  # GRPC::Core::Status and a deserialized Google::Rpc::Status proto
+  class GoogleRpcStatusUtils
+    def self.extract_google_rpc_status(status)
+      fail ArgumentError, 'bad type' unless status.is_a? Struct::Status
+      Google::Rpc::Status.decode(status.metadata['grpc-status-details-bin'])
+    end
+  end
+end
diff --git a/src/ruby/spec/google_rpc_status_utils_spec.rb b/src/ruby/spec/google_rpc_status_utils_spec.rb
new file mode 100644
index 0000000..fe221c3
--- /dev/null
+++ b/src/ruby/spec/google_rpc_status_utils_spec.rb
@@ -0,0 +1,223 @@
+# Copyright 2017 gRPC authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'grpc'
+require_relative '../lib/grpc/google_rpc_status_utils'
+require_relative '../pb/src/proto/grpc/testing/messages_pb'
+require_relative '../pb/src/proto/grpc/testing/messages_pb'
+require 'google/protobuf/well_known_types'
+
+include GRPC::Core
+
+describe 'conversion from a status struct to a google protobuf status' do
+  it 'fails if the input is not a status struct' do
+    begin
+      GRPC::GoogleRpcStatusUtils.extract_google_rpc_status('string')
+    rescue => e
+      exception = e
+    end
+    expect(exception.is_a?(ArgumentError)).to be true
+    expect(exception.message.include?('bad type')).to be true
+  end
+
+  it 'fails with some error if the header key is missing' do
+    status = Struct::Status.new(1, 'details', key: 'val')
+    expect(status.metadata.nil?).to be false
+    expect do
+      GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
+    end.to raise_error(StandardError)
+  end
+
+  it 'fails with some error if the header key fails to deserialize' do
+    status = Struct::Status.new(1, 'details',
+                                'grpc-status-details-bin' => 'string_val')
+    expect do
+      GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
+    end.to raise_error(StandardError)
+  end
+
+  it 'silently ignores erroneous mismatch between messages in '\
+    'status struct and protobuf status' do
+    proto = Google::Rpc::Status.new(code: 1, message: 'proto message')
+    encoded_proto = Google::Rpc::Status.encode(proto)
+    status = Struct::Status.new(1, 'struct message',
+                                'grpc-status-details-bin' => encoded_proto)
+    rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
+    expect(rpc_status).to eq(proto)
+  end
+
+  it 'silently ignores erroneous mismatch between codes in status struct '\
+    'and protobuf status' do
+    proto = Google::Rpc::Status.new(code: 1, message: 'matching message')
+    encoded_proto = Google::Rpc::Status.encode(proto)
+    status = Struct::Status.new(2, 'matching message',
+                                'grpc-status-details-bin' => encoded_proto)
+    rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
+    expect(rpc_status).to eq(proto)
+  end
+
+  it 'can succesfully convert a status struct into a google protobuf status '\
+    'when there are no rpcstatus details' do
+    proto = Google::Rpc::Status.new(code: 1, message: 'matching message')
+    encoded_proto = Google::Rpc::Status.encode(proto)
+    status = Struct::Status.new(1, 'matching message',
+                                'grpc-status-details-bin' => encoded_proto)
+    out = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
+    expect(out.code).to eq(1)
+    expect(out.message).to eq('matching message')
+    expect(out.details).to eq([])
+  end
+
+  it 'can succesfully convert a status struct into a google protobuf '\
+    'status when there are multiple rpcstatus details' do
+    simple_request_any = Google::Protobuf::Any.new
+    simple_request = Grpc::Testing::SimpleRequest.new(
+      payload: Grpc::Testing::Payload.new(body: 'request'))
+    simple_request_any.pack(simple_request)
+    simple_response_any = Google::Protobuf::Any.new
+    simple_response = Grpc::Testing::SimpleResponse.new(
+      payload: Grpc::Testing::Payload.new(body: 'response'))
+    simple_response_any.pack(simple_response)
+    payload_any = Google::Protobuf::Any.new
+    payload = Grpc::Testing::Payload.new(body: 'payload')
+    payload_any.pack(payload)
+    proto = Google::Rpc::Status.new(code: 1,
+                                    message: 'matching message',
+                                    details: [
+                                      simple_request_any,
+                                      simple_response_any,
+                                      payload_any
+                                    ])
+    encoded_proto = Google::Rpc::Status.encode(proto)
+    status = Struct::Status.new(1, 'matching message',
+                                'grpc-status-details-bin' => encoded_proto)
+    out = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(status)
+    expect(out.code).to eq(1)
+    expect(out.message).to eq('matching message')
+    expect(out.details[0].unpack(
+             Grpc::Testing::SimpleRequest)).to eq(simple_request)
+    expect(out.details[1].unpack(
+             Grpc::Testing::SimpleResponse)).to eq(simple_response)
+    expect(out.details[2].unpack(
+             Grpc::Testing::Payload)).to eq(payload)
+  end
+end
+
+# Test message
+class EchoMsg
+  def self.marshal(_o)
+    ''
+  end
+
+  def self.unmarshal(_o)
+    EchoMsg.new
+  end
+end
+
+# A test service that fills in the "reserved" grpc-status-details-bin trailer,
+# for client-side testing of GoogleRpcStatus protobuf extraction from trailers.
+class GoogleRpcStatusTestService
+  include GRPC::GenericService
+  rpc :an_rpc, EchoMsg, EchoMsg
+
+  def initialize(encoded_rpc_status)
+    @encoded_rpc_status = encoded_rpc_status
+  end
+
+  def an_rpc(_, _)
+    # TODO: create a server-side utility API for sending a google rpc status.
+    # Applications are not expected to set the grpc-status-details-bin
+    # ("grpc"-fixed and reserved for library use) manually.
+    # Doing so here is only for testing of the client-side api for extracting
+    # a google rpc status, which is useful
+    # when the interacting with a server that does fill in this trailer.
+    fail GRPC::Unknown.new('test message',
+                           'grpc-status-details-bin' => @encoded_rpc_status)
+  end
+end
+
+GoogleRpcStatusTestStub = GoogleRpcStatusTestService.rpc_stub_class
+
+describe 'receving a google rpc status from a remote endpoint' do
+  def start_server(encoded_rpc_status)
+    @srv = GRPC::RpcServer.new(pool_size: 1)
+    @server_port = @srv.add_http2_port('localhost:0',
+                                       :this_port_is_insecure)
+    @srv.handle(GoogleRpcStatusTestService.new(encoded_rpc_status))
+    @server_thd = Thread.new { @srv.run }
+    @srv.wait_till_running
+  end
+
+  def stop_server
+    expect(@srv.stopped?).to be(false)
+    @srv.stop
+    @server_thd.join
+    expect(@srv.stopped?).to be(true)
+  end
+
+  before(:each) do
+    simple_request_any = Google::Protobuf::Any.new
+    simple_request = Grpc::Testing::SimpleRequest.new(
+      payload: Grpc::Testing::Payload.new(body: 'request'))
+    simple_request_any.pack(simple_request)
+    simple_response_any = Google::Protobuf::Any.new
+    simple_response = Grpc::Testing::SimpleResponse.new(
+      payload: Grpc::Testing::Payload.new(body: 'response'))
+    simple_response_any.pack(simple_response)
+    payload_any = Google::Protobuf::Any.new
+    payload = Grpc::Testing::Payload.new(body: 'payload')
+    payload_any.pack(payload)
+    @expected_proto = Google::Rpc::Status.new(
+      code: StatusCodes::UNKNOWN,
+      message: 'test message',
+      details: [simple_request_any, simple_response_any, payload_any])
+    start_server(Google::Rpc::Status.encode(@expected_proto))
+  end
+
+  after(:each) do
+    stop_server
+  end
+
+  it 'should receive be able to extract a google rpc status from the '\
+    'status struct taken from a BadStatus exception' do
+    stub = GoogleRpcStatusTestStub.new("localhost:#{@server_port}",
+                                       :this_channel_is_insecure)
+    begin
+      stub.an_rpc(EchoMsg.new)
+    rescue GRPC::BadStatus => e
+      rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
+        e.to_status)
+    end
+    expect(rpc_status).to eq(@expected_proto)
+  end
+
+  it 'should receive be able to extract a google rpc status from the '\
+    'status struct taken from the op view of a call' do
+    stub = GoogleRpcStatusTestStub.new("localhost:#{@server_port}",
+                                       :this_channel_is_insecure)
+    op = stub.an_rpc(EchoMsg.new, return_op: true)
+    begin
+      op.execute
+    rescue GRPC::BadStatus => e
+      status_from_exception = e.to_status
+    end
+    rpc_status = GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
+      op.status)
+    expect(rpc_status).to eq(@expected_proto)
+    # "to_status" on the bad status should give the same result
+    # as "status" on the "op view".
+    expect(GRPC::GoogleRpcStatusUtils.extract_google_rpc_status(
+             status_from_exception)).to eq(rpc_status)
+  end
+end
diff --git a/templates/grpc.gemspec.template b/templates/grpc.gemspec.template
index 8b57993..e946a6a 100644
--- a/templates/grpc.gemspec.template
+++ b/templates/grpc.gemspec.template
@@ -31,6 +31,7 @@
 
     s.add_dependency 'google-protobuf', '~> 3.1'
     s.add_dependency 'googleauth',      '~> 0.5.1'
+    s.add_dependency 'googleapis-common-protos-types', '~> 1.0.0'
 
     s.add_development_dependency 'bundler',            '~> 1.9'
     s.add_development_dependency 'facter',             '~> 2.4'