Skip to main content

ploidy_codegen_rust/
client.rs

1use ploidy_core::codegen::IntoCode;
2use proc_macro2::TokenStream;
3use quote::{ToTokens, TokenStreamExt, quote};
4
5use super::{
6    cfg::CfgFeature,
7    graph::CodegenGraph,
8    naming::{CargoFeature, CodegenIdentUsage},
9};
10
11/// Generates the `client/mod.rs` source file.
12#[derive(Clone, Copy, Debug)]
13pub struct CodegenClientModule<'a> {
14    graph: &'a CodegenGraph<'a>,
15    features: &'a [&'a CargoFeature],
16}
17
18impl<'a> CodegenClientModule<'a> {
19    pub fn new(graph: &'a CodegenGraph<'a>, features: &'a [&'a CargoFeature]) -> Self {
20        Self { graph, features }
21    }
22}
23
24impl ToTokens for CodegenClientModule<'_> {
25    fn to_tokens(&self, tokens: &mut TokenStream) {
26        let mods = self.features.iter().map(|feature| {
27            let cfg = CfgFeature::for_resource_module(feature);
28            let mod_name = CodegenIdentUsage::Module(feature.as_ident());
29            quote! {
30                #cfg
31                pub mod #mod_name;
32            }
33        });
34
35        let client_doc = self.graph.info().label().map(|label| {
36            let doc = match label.version {
37                Some(version) => format!("API client for {} (version {version})", label.title),
38                None => format!("API client for {}", label.title),
39            };
40            quote! { #[doc = #doc] }
41        });
42
43        tokens.append_all(quote! {
44            #client_doc
45            #[derive(Clone, Debug)]
46            pub struct Client {
47                client: ::ploidy_util::reqwest::Client,
48                headers: ::ploidy_util::http::HeaderMap,
49                base_url: ::ploidy_util::url::Url,
50            }
51
52            impl Client {
53                /// Creates a new client.
54                pub fn new(base_url: impl AsRef<str>) -> Result<Self, crate::error::Error> {
55                    Ok(Self::with_reqwest_client(
56                        ::ploidy_util::reqwest::Client::new(),
57                        base_url.as_ref().parse()?,
58                    ))
59                }
60
61                pub fn with_reqwest_client(
62                    client: crate::util::reqwest::Client,
63                    base_url: crate::util::url::Url,
64                ) -> Self {
65                    Self {
66                        client,
67                        headers: ::ploidy_util::http::HeaderMap::new(),
68                        base_url,
69                    }
70                }
71
72                /// Adds a header to each request.
73                pub fn with_header<K, V>(mut self, name: K, value: V) -> Result<Self, crate::error::Error>
74                where
75                    K: TryInto<crate::util::http::HeaderName>,
76                    V: TryInto<crate::util::http::HeaderValue>,
77                    K::Error: Into<crate::util::http::Error>,
78                    V::Error: Into<crate::util::http::Error>,
79                {
80                    let name = name
81                        .try_into()
82                        .map_err(|err| crate::error::Error::BadHeaderName(err.into()))?;
83                    let value = value
84                        .try_into()
85                        .map_err(|err| crate::error::Error::BadHeaderValue(name.clone(), err.into()))?;
86                    self.headers.insert(name, value);
87                    Ok(Self {
88                        client: self.client,
89                        headers: self.headers,
90                        base_url: self.base_url,
91                    })
92                }
93
94                /// Adds a sensitive header to each request, like a password or a bearer token.
95                /// Sensitive headers won't appear in `Debug` output, and may be treated specially
96                /// by the underlying HTTP stack.
97                ///
98                /// # Example
99                ///
100                /// ```rust,ignore
101                /// use reqwest::header::AUTHORIZATION;
102                ///
103                /// let client = Client::new("https://api.example.com")?
104                ///     .with_sensitive_header(AUTHORIZATION, "Bearer decafbadcafed00d")?;
105                /// ```
106                pub fn with_sensitive_header<K, V>(self, name: K, value: V) -> Result<Self, crate::error::Error>
107                where
108                    K: TryInto<crate::util::http::HeaderName>,
109                    V: TryInto<crate::util::http::HeaderValue>,
110                    K::Error: Into<crate::util::http::Error>,
111                    V::Error: Into<crate::util::http::Error>,
112                {
113                    let name = name
114                        .try_into()
115                        .map_err(|err| crate::error::Error::BadHeaderName(err.into()))?;
116                    let mut value: ::ploidy_util::http::HeaderValue = value
117                        .try_into()
118                        .map_err(|err| crate::error::Error::BadHeaderValue(name.clone(), err.into()))?;
119                    value.set_sensitive(true);
120                    self.with_header(name, value)
121                }
122
123                pub fn with_user_agent<V>(self, value: V) -> Result<Self, crate::error::Error>
124                where
125                    V: TryInto<crate::util::http::HeaderValue>,
126                    V::Error: Into<crate::util::http::Error>,
127                {
128                    self.with_header(::ploidy_util::http::header::USER_AGENT, value)
129                }
130
131                /// Returns a raw [`RequestBuilder`].
132                ///
133                /// Constructs the request URL by appending `path_and_query`
134                /// to the base URL's path and query, respectively. For example,
135                /// given a base URL of `https://api.example.com/v1` and a
136                /// `path_and_query` of `/pets/list?limit=10`, the request URL is
137                /// `https://api.example.com/v1/pets/list?limit=10`.
138                ///
139                /// The request includes the client's default headers.
140                ///
141                /// Use this for requests that the typed client methods
142                /// don't support.
143                ///
144                /// [`RequestBuilder`]: crate::util::reqwest::RequestBuilder
145                pub fn request(
146                    &self,
147                    method: crate::util::reqwest::Method,
148                    path_and_query: &str,
149                ) -> Result<crate::util::reqwest::RequestBuilder, crate::error::Error> {
150                    let parts: ::ploidy_util::http::uri::PathAndQuery = path_and_query.parse()?;
151                    let mut url = self.base_url.clone();
152                    let _ = url.path_segments_mut().map(|mut segments| {
153                        let path = parts.path();
154                        if path != "/" {
155                            let path = path
156                                .strip_prefix('/') // Drop leading `/` from new path.
157                                .unwrap_or(path);
158                            segments
159                                .pop_if_empty() // Drop trailing `/` from the base path.
160                                .extend(path.split('/'));
161                        }
162                    });
163                    if let Some(query) = parts.query() {
164                        url.query_pairs_mut()
165                            .extend_pairs(::ploidy_util::url::form_urlencoded::parse(query.as_bytes()));
166                    }
167                    Ok(self.client
168                        .request(method, url)
169                        .headers(self.headers.clone()))
170                }
171            }
172
173            #(#mods)*
174        });
175    }
176}
177
178impl IntoCode for CodegenClientModule<'_> {
179    type Code = (&'static str, TokenStream);
180
181    fn into_code(self) -> Self::Code {
182        ("src/client/mod.rs", self.into_token_stream())
183    }
184}