tonic_build_codec/
client.rs

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