pw_rpc: Add ClientServer combination

This adds a class which wraps both an RPC client and server, simplifying
setup and usage in systems that require both.

Change-Id: I00e3cbeef91b8703c432800f58a96db5faff63f4
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/40624
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Alexei Frolov <frolv@google.com>
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index 996a8a0..b2cd789 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -63,6 +63,16 @@
 )
 
 pw_cc_library(
+    name = "client_server",
+    srcs = ["client_server.cc"],
+    hdrs = ["public/pw_rpc/client_server.h"],
+    deps = [
+        ":client",
+        ":server",
+    ],
+)
+
+pw_cc_library(
     name = "common",
     srcs = [
         "channel.cc",
@@ -186,6 +196,15 @@
     ],
 )
 
+pw_cc_test(
+    name = "client_server_test",
+    srcs = ["client_server_test.cc"],
+    deps = [
+        ":client_server",
+        "//pw_rpc/raw:method_union",
+    ],
+)
+
 proto_library(
     name = "packet_proto",
     srcs = [
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 5f98883..9f4a95f 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -82,6 +82,16 @@
   ]
 }
 
+pw_source_set("client_server") {
+  public_configs = [ ":public_include_path" ]
+  public_deps = [
+    ":client",
+    ":server",
+  ]
+  public = [ "public/pw_rpc/client_server.h" ]
+  sources = [ "client_server.cc" ]
+}
+
 # Classes shared by the server and client.
 pw_source_set("common") {
   public_configs = [ ":public_include_path" ]
@@ -184,6 +194,7 @@
     ":base_server_writer_test",
     ":channel_test",
     ":client_test",
+    ":client_server_test",
     ":ids_test",
     ":packet_test",
     ":server_test",
@@ -262,6 +273,15 @@
   sources = [ "client_test.cc" ]
 }
 
+pw_test("client_server_test") {
+  deps = [
+    ":client_server",
+    ":test_utils",
+    "raw:method_union",
+  ]
+  sources = [ "client_server_test.cc" ]
+}
+
 pw_test("base_client_call_test") {
   deps = [
     ":client",
diff --git a/pw_rpc/CMakeLists.txt b/pw_rpc/CMakeLists.txt
index b929d5a..c3635fe 100644
--- a/pw_rpc/CMakeLists.txt
+++ b/pw_rpc/CMakeLists.txt
@@ -43,6 +43,14 @@
     pw_log
 )
 
+pw_add_module_library(pw_rpc.client_server
+  SOURCES
+    client_server.cc
+  PUBLIC_DEPS
+    pw_rpc.client
+    pw_rpc.server
+)
+
 pw_add_module_library(pw_rpc.common
   SOURCES
     channel.cc
diff --git a/pw_rpc/client_server.cc b/pw_rpc/client_server.cc
new file mode 100644
index 0000000..f0c34ab
--- /dev/null
+++ b/pw_rpc/client_server.cc
@@ -0,0 +1,30 @@
+// Copyright 2021 The Pigweed 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
+//
+//     https://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.
+
+#include "pw_rpc/client_server.h"
+
+namespace pw::rpc {
+
+Status ClientServer::ProcessPacket(std::span<const std::byte> packet,
+                                   ChannelOutput& interface) {
+  Status status = server_.ProcessPacket(packet, interface);
+  if (status.IsInvalidArgument()) {
+    // INVALID_ARGUMENT indicates the packet is intended for a client.
+    status = client_.ProcessPacket(packet);
+  }
+
+  return status;
+}
+
+}  // namespace pw::rpc
diff --git a/pw_rpc/client_server_test.cc b/pw_rpc/client_server_test.cc
new file mode 100644
index 0000000..5104c66
--- /dev/null
+++ b/pw_rpc/client_server_test.cc
@@ -0,0 +1,85 @@
+// Copyright 2020 The Pigweed 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
+//
+//     https://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.
+
+#include "pw_rpc/client_server.h"
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/internal/raw_method_union.h"
+#include "pw_rpc/server_context.h"
+#include "pw_rpc/service.h"
+#include "pw_rpc_private/internal_test_utils.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+constexpr uint32_t kFakeChannelId = 1;
+constexpr uint32_t kFakeServiceId = 3;
+constexpr uint32_t kFakeMethodId = 10;
+
+TestOutput<32> output;
+rpc::Channel channels[] = {Channel::Create<kFakeChannelId>(&output)};
+
+StatusWithSize FakeMethod(ServerContext&, ConstByteSpan, ByteSpan) {
+  return StatusWithSize::Unimplemented();
+}
+
+class FakeService : public Service {
+ public:
+  FakeService(uint32_t id) : Service(id, kMethods) {}
+
+  static constexpr std::array<RawMethodUnion, 1> kMethods = {
+      RawMethod::Unary<FakeMethod>(kFakeMethodId),
+  };
+};
+
+FakeService service(kFakeServiceId);
+
+TEST(ClientServer, ProcessPacket_CallsServer) {
+  ClientServer client_server(channels);
+  client_server.server().RegisterService(service);
+
+  Packet packet(
+      PacketType::REQUEST, kFakeChannelId, kFakeServiceId, kFakeMethodId);
+  std::array<std::byte, 32> buffer;
+  Result result = packet.Encode(buffer);
+  EXPECT_EQ(result.status(), OkStatus());
+
+  EXPECT_EQ(client_server.ProcessPacket(result.value(), output), OkStatus());
+}
+
+TEST(ClientServer, ProcessPacket_CallsClient) {
+  ClientServer client_server(channels);
+  client_server.server().RegisterService(service);
+
+  // Same packet as above, but type RESPONSE will skip the server and call into
+  // the client.
+  Packet packet(
+      PacketType::RESPONSE, kFakeChannelId, kFakeServiceId, kFakeMethodId);
+  std::array<std::byte, 32> buffer;
+  Result result = packet.Encode(buffer);
+  EXPECT_EQ(result.status(), OkStatus());
+
+  // No calls are registered on the client, so this should fail.
+  EXPECT_EQ(client_server.ProcessPacket(result.value(), output),
+            Status::NotFound());
+}
+
+TEST(ClientServer, ProcessPacket_BadData) {
+  ClientServer client_server(channels);
+  EXPECT_EQ(client_server.ProcessPacket({}, output), Status::DataLoss());
+}
+
+}  // namespace
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index fc04165..daece79 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -848,3 +848,26 @@
 The RPC server stores a list of all of active ``ClientCall`` objects. When an
 incoming packet is recieved, it dispatches to one of its active calls, which
 then decodes the payload and presents it to the user.
+
+ClientServer
+============
+Sometimes, a device needs to both process RPCs as a server, as well as making
+calls to another device as a client. To do this, both a client and server must
+be set up, and incoming packets must be sent to both of them.
+
+Pigweed simplifies this setup by providing a ``ClientServer`` class which wraps
+an RPC client and server with the same set of channels.
+
+.. code-block:: cpp
+
+  pw::rpc::Channel channels[] = {
+      pw::rpc::Channel::Create<1>(&channel_output)};
+
+  // Creates both a client and a server.
+  pw::rpc::ClientServer client_server(channels);
+
+  void ProcessRpcData(pw::ConstByteSpan packet) {
+    // Calls into both the client and the server, sending the packet to the
+    // appropriate one.
+    client_server.ProcessPacket(packet, output);
+  }
diff --git a/pw_rpc/public/pw_rpc/client_server.h b/pw_rpc/public/pw_rpc/client_server.h
new file mode 100644
index 0000000..219ed67
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/client_server.h
@@ -0,0 +1,40 @@
+// Copyright 2021 The Pigweed 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
+//
+//     https://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.
+#pragma once
+
+#include "pw_rpc/client.h"
+#include "pw_rpc/server.h"
+
+namespace pw::rpc {
+
+// Class that wraps both an RPC client and a server, simplifying RPC setup when
+// a device needs to function as both.
+class ClientServer {
+ public:
+  constexpr ClientServer(std::span<Channel> channels)
+      : client_(channels), server_(channels) {}
+
+  // Sends a packet to either the client or the server, depending on its type.
+  Status ProcessPacket(std::span<const std::byte> packet,
+                       ChannelOutput& interface);
+
+  constexpr Client& client() { return client_; }
+  constexpr Server& server() { return server_; }
+
+ private:
+  Client client_;
+  Server server_;
+};
+
+}  // namespace pw::rpc