xref: /tonic/tonic-build/src/client.rs (revision 63f2e95b)
1 use std::collections::HashSet;
2 
3 use super::{Attributes, Method, Service};
4 use crate::{format_method_name, generate_doc_comments, naive_snake_case};
5 use proc_macro2::TokenStream;
6 use quote::{format_ident, quote};
7 
8 /// Generate service for client.
9 ///
10 /// This takes some `Service` and will generate a `TokenStream` that contains
11 /// a public module with the generated client.
12 #[deprecated(since = "0.8.3", note = "Use the CodeGenBuilder::generate_client")]
13 pub fn generate<T: Service>(
14     service: &T,
15     emit_package: bool,
16     proto_path: &str,
17     compile_well_known_types: bool,
18     build_transport: bool,
19     attributes: &Attributes,
20 ) -> TokenStream {
21     generate_internal(
22         service,
23         emit_package,
24         proto_path,
25         compile_well_known_types,
26         build_transport,
27         attributes,
28         &HashSet::default(),
29     )
30 }
31 
32 pub(crate) fn generate_internal<T: Service>(
33     service: &T,
34     emit_package: bool,
35     proto_path: &str,
36     compile_well_known_types: bool,
37     build_transport: bool,
38     attributes: &Attributes,
39     disable_comments: &HashSet<String>,
40 ) -> TokenStream {
41     let service_ident = quote::format_ident!("{}Client", service.name());
42     let client_mod = quote::format_ident!("{}_client", naive_snake_case(service.name()));
43     let methods = generate_methods(
44         service,
45         emit_package,
46         proto_path,
47         compile_well_known_types,
48         disable_comments,
49     );
50 
51     let connect = generate_connect(&service_ident, build_transport);
52 
53     let package = if emit_package { service.package() } else { "" };
54     let path = format!(
55         "{}{}{}",
56         package,
57         if package.is_empty() { "" } else { "." },
58         service.identifier()
59     );
60 
61     let service_doc = if disable_comments.contains(&path) {
62         TokenStream::new()
63     } else {
64         generate_doc_comments(service.comment())
65     };
66 
67     let mod_attributes = attributes.for_mod(package);
68     let struct_attributes = attributes.for_struct(&path);
69 
70     quote! {
71         /// Generated client implementations.
72         #(#mod_attributes)*
73         pub mod #client_mod {
74             #![allow(
75                 unused_variables,
76                 dead_code,
77                 missing_docs,
78                 // will trigger if compression is disabled
79                 clippy::let_unit_value,
80             )]
81             use tonic::codegen::*;
82             use tonic::codegen::http::Uri;
83 
84             #service_doc
85             #(#struct_attributes)*
86             #[derive(Debug, Clone)]
87             pub struct #service_ident<T> {
88                 inner: tonic::client::Grpc<T>,
89             }
90 
91             #connect
92 
93             impl<T> #service_ident<T>
94             where
95                 T: tonic::client::GrpcService<tonic::body::BoxBody>,
96                 T::Error: Into<StdError>,
97                 T::ResponseBody: Body<Data = Bytes> + Send  + 'static,
98                 <T::ResponseBody as Body>::Error: Into<StdError> + Send,
99             {
100                 pub fn new(inner: T) -> Self {
101                     let inner = tonic::client::Grpc::new(inner);
102                     Self { inner }
103                 }
104 
105                 pub fn with_origin(inner: T, origin: Uri) -> Self {
106                     let inner = tonic::client::Grpc::with_origin(inner, origin);
107                     Self { inner }
108                 }
109 
110                 pub fn with_interceptor<F>(inner: T, interceptor: F) -> #service_ident<InterceptedService<T, F>>
111                 where
112                     F: tonic::service::Interceptor,
113                     T::ResponseBody: Default,
114                     T: tonic::codegen::Service<
115                         http::Request<tonic::body::BoxBody>,
116                         Response = http::Response<<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody>
117                     >,
118                     <T as tonic::codegen::Service<http::Request<tonic::body::BoxBody>>>::Error: Into<StdError> + Send + Sync,
119                 {
120                     #service_ident::new(InterceptedService::new(inner, interceptor))
121                 }
122 
123                 /// Compress requests with the given encoding.
124                 ///
125                 /// This requires the server to support it otherwise it might respond with an
126                 /// error.
127                 #[must_use]
128                 pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
129                     self.inner = self.inner.send_compressed(encoding);
130                     self
131                 }
132 
133                 /// Enable decompressing responses.
134                 #[must_use]
135                 pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
136                     self.inner = self.inner.accept_compressed(encoding);
137                     self
138                 }
139 
140                 #methods
141             }
142         }
143     }
144 }
145 
146 #[cfg(feature = "transport")]
147 fn generate_connect(service_ident: &syn::Ident, enabled: bool) -> TokenStream {
148     let connect_impl = quote! {
149         impl #service_ident<tonic::transport::Channel> {
150             /// Attempt to create a new client by connecting to a given endpoint.
151             pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
152             where
153                 D: TryInto<tonic::transport::Endpoint>,
154                 D::Error: Into<StdError>,
155             {
156                 let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
157                 Ok(Self::new(conn))
158             }
159         }
160     };
161 
162     if enabled {
163         connect_impl
164     } else {
165         TokenStream::new()
166     }
167 }
168 
169 #[cfg(not(feature = "transport"))]
170 fn generate_connect(_service_ident: &syn::Ident, _enabled: bool) -> TokenStream {
171     TokenStream::new()
172 }
173 
174 fn generate_methods<T: Service>(
175     service: &T,
176     emit_package: bool,
177     proto_path: &str,
178     compile_well_known_types: bool,
179     disable_comments: &HashSet<String>,
180 ) -> TokenStream {
181     let mut stream = TokenStream::new();
182     let package = if emit_package { service.package() } else { "" };
183 
184     for method in service.methods() {
185         let path = format!(
186             "/{}{}{}/{}",
187             package,
188             if package.is_empty() { "" } else { "." },
189             service.identifier(),
190             method.identifier()
191         );
192 
193         if !disable_comments.contains(&format_method_name(package, service, method)) {
194             stream.extend(generate_doc_comments(method.comment()));
195         }
196 
197         let method = match (method.client_streaming(), method.server_streaming()) {
198             (false, false) => generate_unary(method, proto_path, compile_well_known_types, path),
199             (false, true) => {
200                 generate_server_streaming(method, proto_path, compile_well_known_types, path)
201             }
202             (true, false) => {
203                 generate_client_streaming(method, proto_path, compile_well_known_types, path)
204             }
205             (true, true) => generate_streaming(method, proto_path, compile_well_known_types, path),
206         };
207 
208         stream.extend(method);
209     }
210 
211     stream
212 }
213 
214 fn generate_unary<T: Method>(
215     method: &T,
216     proto_path: &str,
217     compile_well_known_types: bool,
218     path: String,
219 ) -> TokenStream {
220     let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
221     let ident = format_ident!("{}", method.name());
222     let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
223 
224     quote! {
225         pub async fn #ident(
226             &mut self,
227             request: impl tonic::IntoRequest<#request>,
228         ) -> std::result::Result<tonic::Response<#response>, tonic::Status> {
229            self.inner.ready().await.map_err(|e| {
230                tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into()))
231            })?;
232            let codec = #codec_name::default();
233            let path = http::uri::PathAndQuery::from_static(#path);
234            self.inner.unary(request.into_request(), path, codec).await
235         }
236     }
237 }
238 
239 fn generate_server_streaming<T: Method>(
240     method: &T,
241     proto_path: &str,
242     compile_well_known_types: bool,
243     path: String,
244 ) -> TokenStream {
245     let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
246     let ident = format_ident!("{}", method.name());
247 
248     let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
249 
250     quote! {
251         pub async fn #ident(
252             &mut self,
253             request: impl tonic::IntoRequest<#request>,
254         ) -> std::result::Result<tonic::Response<tonic::codec::Streaming<#response>>, tonic::Status> {
255             self.inner.ready().await.map_err(|e| {
256                         tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into()))
257             })?;
258             let codec = #codec_name::default();
259             let path = http::uri::PathAndQuery::from_static(#path);
260             self.inner.server_streaming(request.into_request(), path, codec).await
261         }
262     }
263 }
264 
265 fn generate_client_streaming<T: Method>(
266     method: &T,
267     proto_path: &str,
268     compile_well_known_types: bool,
269     path: String,
270 ) -> TokenStream {
271     let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
272     let ident = format_ident!("{}", method.name());
273 
274     let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
275 
276     quote! {
277         pub async fn #ident(
278             &mut self,
279             request: impl tonic::IntoStreamingRequest<Message = #request>
280         ) -> std::result::Result<tonic::Response<#response>, tonic::Status> {
281             self.inner.ready().await.map_err(|e| {
282                         tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into()))
283             })?;
284             let codec = #codec_name::default();
285             let path = http::uri::PathAndQuery::from_static(#path);
286             self.inner.client_streaming(request.into_streaming_request(), path, codec).await
287         }
288     }
289 }
290 
291 fn generate_streaming<T: Method>(
292     method: &T,
293     proto_path: &str,
294     compile_well_known_types: bool,
295     path: String,
296 ) -> TokenStream {
297     let codec_name = syn::parse_str::<syn::Path>(method.codec_path()).unwrap();
298     let ident = format_ident!("{}", method.name());
299 
300     let (request, response) = method.request_response_name(proto_path, compile_well_known_types);
301 
302     quote! {
303         pub async fn #ident(
304             &mut self,
305             request: impl tonic::IntoStreamingRequest<Message = #request>
306         ) -> std::result::Result<tonic::Response<tonic::codec::Streaming<#response>>, tonic::Status> {
307             self.inner.ready().await.map_err(|e| {
308                         tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into()))
309             })?;
310             let codec = #codec_name::default();
311             let path = http::uri::PathAndQuery::from_static(#path);
312             self.inner.streaming(request.into_streaming_request(), path, codec).await
313         }
314     }
315 }
316