1 // Copyright 2020 The Pigweed Authors
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
7 // https://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
19 #include "pw_assert/assert.h"
20 #include "pw_containers/vector.h"
21 #include "pw_preprocessor/arguments.h"
22 #include "pw_rpc/channel.h"
23 #include "pw_rpc/internal/hash.h"
24 #include "pw_rpc/internal/nanopb_method.h"
25 #include "pw_rpc/internal/packet.h"
26 #include "pw_rpc/internal/server.h"
27 #include "pw_rpc/internal/service_method_traits.h"
31 // Declares a context object that may be used to invoke an RPC. The context is
32 // declared with a pointer to the service member function (&Service::Method).
33 // The RPC can then be invoked with the call method.
35 // For a unary RPC, context.call(request) returns the status, and the response
36 // struct can be accessed via context.response().
38 // pw::rpc::TestMethodContext<&my::CoolService::TheMethod> context;
39 // EXPECT_EQ(Status::Ok(), context.call({.some_arg = 123}));
40 // EXPECT_EQ(500, context.response().some_response_value);
42 // For a server streaming RPC, context.call(request) invokes the method. As in a
43 // normal RPC, the method completes when the ServerWriter's Finish method is
44 // called (or it goes out of scope).
46 // pw::rpc::TestMethodContext<&my::CoolService::TheStreamingMethod> context;
47 // context.call({.some_arg = 123});
49 // EXPECT_TRUE(context.done()); // Check that the RPC completed
50 // EXPECT_EQ(Status::Ok(), context.status()); // Check the status
52 // EXPECT_EQ(3u, context.responses().size());
53 // EXPECT_EQ(123, context.responses()[0].value); // check individual responses
55 // for (const MyResponse& response : context.responses()) {
56 // // iterate over the responses
59 // TestMethodContext forwards its constructor arguments to the underlying
60 // serivce. For example:
62 // pw::rpc::TestMethodContext<&MyService::Go> context(serivce, args);
64 // pw::rpc::TestMethodContext takes two optional template arguments:
66 // size_t max_responses: maximum responses to store; ignored unless streaming
67 // size_t output_size_bytes: buffer size; must be large enough for a packet
71 // pw::rpc::TestMethodContext<&MyService::BestMethod, 3, 256> context;
72 // ASSERT_EQ(3u, context.responses().max_size());
74 template <auto method, size_t max_responses = 4, size_t output_size_bytes = 128>
75 class TestMethodContext;
77 // Internal classes that implement TestMethodContext.
78 namespace internal::test {
80 // A ChannelOutput implementation that stores the outgoing payloads and status.
81 template <typename Response>
82 class MessageOutput final : public ChannelOutput {
84 MessageOutput(const internal::NanopbMethod& method,
85 Vector<Response>& responses,
86 std::span<std::byte> buffer)
87 : ChannelOutput("internal::test::MessageOutput"),
89 responses_(responses),
94 Status last_status() const { return last_status_; }
95 void set_last_status(Status status) { last_status_ = status; }
97 size_t total_responses() const { return total_responses_; }
99 bool stream_ended() const { return stream_ended_; }
104 std::span<std::byte> AcquireBuffer() override { return buffer_; }
106 Status SendAndReleaseBuffer(size_t size) override;
108 const internal::NanopbMethod& method_;
109 Vector<Response>& responses_;
110 std::span<std::byte> buffer_;
111 size_t total_responses_;
116 // Collects everything needed to invoke a particular RPC.
117 template <auto method, size_t max_responses, size_t output_size>
118 struct InvocationContext {
119 using Request = internal::Request<method>;
120 using Response = internal::Response<method>;
122 template <typename... Args>
123 InvocationContext(Args&&... args)
124 : output(ServiceMethodTraits<method>::method(), responses, buffer),
125 channel(Channel::Create<123>(&output)),
126 server(std::span(&channel, 1)),
127 service(std::forward<Args>(args)...),
128 call(static_cast<internal::Server&>(server),
129 static_cast<internal::Channel&>(channel),
131 ServiceMethodTraits<method>::method()) {}
133 MessageOutput<Response> output;
135 rpc::Channel channel;
137 typename ServiceMethodTraits<method>::Service service;
138 Vector<Response, max_responses> responses;
139 std::array<std::byte, output_size> buffer = {};
141 internal::ServerCall call;
144 // Method invocation context for a unary RPC. Returns the status in call() and
145 // provides the response through the response() method.
146 template <auto method, size_t output_size>
149 InvocationContext<method, 1, output_size> ctx_;
152 using Request = typename decltype(ctx_)::Request;
153 using Response = typename decltype(ctx_)::Response;
155 template <typename... Args>
156 UnaryContext(Args&&... args) : ctx_(std::forward<Args>(args)...) {}
158 // Invokes the RPC with the provided request. Returns the status.
159 Status call(const Request& request) {
161 ctx_.responses.emplace_back();
162 ctx_.responses.back() = {};
163 return (ctx_.service.*method)(
164 ctx_.call.context(), request, ctx_.responses.back());
167 // Gives access to the RPC's response.
168 const Response& response() const {
169 PW_CHECK_UINT_GT(ctx_.responses.size(), 0);
170 return ctx_.responses.back();
174 // Method invocation context for a server streaming RPC.
175 template <auto method, size_t max_responses, size_t output_size>
176 class ServerStreamingContext {
178 InvocationContext<method, max_responses, output_size> ctx_;
181 using Request = typename decltype(ctx_)::Request;
182 using Response = typename decltype(ctx_)::Response;
184 template <typename... Args>
185 ServerStreamingContext(Args&&... args) : ctx_(std::forward<Args>(args)...) {}
187 // Invokes the RPC with the provided request.
188 void call(const Request& request) {
190 internal::BaseServerWriter server_writer(ctx_.call);
191 return (ctx_.service.*method)(
194 static_cast<ServerWriter<Response>&>(server_writer));
197 // Returns a server writer which writes responses into the context's buffer.
198 // This should not be called alongside call(); use one or the other.
199 ServerWriter<Response> writer() {
201 internal::BaseServerWriter server_writer(ctx_.call);
202 return std::move(static_cast<ServerWriter<Response>&>(server_writer));
205 // Returns the responses that have been recorded. The maximum number of
206 // responses is responses().max_size(). responses().back() is always the most
207 // recent response, even if total_responses() > responses().max_size().
208 const Vector<Response>& responses() const { return ctx_.responses; }
210 // The total number of responses sent, which may be larger than
211 // responses.max_size().
212 size_t total_responses() const { return ctx_.output.total_responses(); }
214 // True if the stream has terminated.
215 bool done() const { return ctx_.output.stream_ended(); }
217 // The status of the stream. Only valid if done() is true.
218 Status status() const {
220 return ctx_.output.last_status();
224 // Alias to select the type of the context object to use based on which type of
226 template <auto method, size_t responses, size_t output_size>
227 using Context = std::tuple_element_t<
228 static_cast<size_t>(internal::RpcTraits<decltype(method)>::kType),
230 internal::test::UnaryContext<method, output_size>,
231 internal::test::ServerStreamingContext<method, responses, output_size>
232 // TODO(hepler): Support client and bidi streaming
235 template <typename Response>
236 void MessageOutput<Response>::clear() {
238 total_responses_ = 0;
239 stream_ended_ = false;
240 last_status_ = Status::Unknown();
243 template <typename Response>
244 Status MessageOutput<Response>::SendAndReleaseBuffer(size_t size) {
245 PW_CHECK(!stream_ended_);
251 Result<internal::Packet> result =
252 internal::Packet::FromBuffer(std::span(buffer_.data(), size));
254 last_status_ = result.status();
256 switch (result.value().type()) {
257 case internal::PacketType::RESPONSE:
258 // If we run out of space, the back message is always the most recent.
259 responses_.emplace_back();
260 responses_.back() = {};
262 method_.DecodeResponse(result.value().payload(), &responses_.back()));
263 total_responses_ += 1;
265 case internal::PacketType::SERVER_STREAM_END:
266 stream_ended_ = true;
269 PW_CRASH("Unhandled PacketType");
274 } // namespace internal::test
276 template <auto method, size_t max_responses, size_t output_size_bytes>
277 class TestMethodContext
278 : public internal::test::Context<method, max_responses, output_size_bytes> {
280 // Forwards constructor arguments to the service class.
281 template <typename... ServiceArgs>
282 TestMethodContext(ServiceArgs&&... service_args)
283 : internal::test::Context<method, max_responses, output_size_bytes>(
284 std::forward<ServiceArgs>(service_args)...) {}
287 } // namespace pw::rpc