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 = {
36            let info = self.graph.info();
37            format!("API client for {} (version {})", info.title, info.version)
38        };
39
40        tokens.append_all(quote! {
41            #[doc = #client_doc]
42            #[derive(Clone, Debug)]
43            pub struct Client {
44                client: ::ploidy_util::reqwest::Client,
45                headers: ::ploidy_util::http::HeaderMap,
46                base_url: ::ploidy_util::url::Url,
47            }
48
49            impl Client {
50                /// Create a new client.
51                pub fn new(base_url: impl AsRef<str>) -> Result<Self, crate::error::Error> {
52                    Ok(Self::with_reqwest_client(
53                        ::ploidy_util::reqwest::Client::new(),
54                        base_url.as_ref().parse()?,
55                    ))
56                }
57
58                pub fn with_reqwest_client(
59                    client: crate::util::reqwest::Client,
60                    base_url: crate::util::url::Url,
61                ) -> Self {
62                    Self {
63                        client,
64                        headers: ::ploidy_util::http::HeaderMap::new(),
65                        base_url,
66                    }
67                }
68
69                /// Adds a header to each request.
70                pub fn with_header<K, V>(mut self, name: K, value: V) -> Result<Self, crate::error::Error>
71                where
72                    K: TryInto<crate::util::http::HeaderName>,
73                    V: TryInto<crate::util::http::HeaderValue>,
74                    K::Error: Into<crate::util::http::Error>,
75                    V::Error: Into<crate::util::http::Error>,
76                {
77                    let name = name
78                        .try_into()
79                        .map_err(|err| crate::error::Error::BadHeaderName(err.into()))?;
80                    let value = value
81                        .try_into()
82                        .map_err(|err| crate::error::Error::BadHeaderValue(name.clone(), err.into()))?;
83                    self.headers.insert(name, value);
84                    Ok(Self {
85                        client: self.client,
86                        headers: self.headers,
87                        base_url: self.base_url,
88                    })
89                }
90
91                /// Adds a sensitive header to each request, like a password or a bearer token.
92                /// Sensitive headers won't appear in `Debug` output, and may be treated specially
93                /// by the underlying HTTP stack.
94                ///
95                /// # Example
96                ///
97                /// ```rust,ignore
98                /// use reqwest::header::AUTHORIZATION;
99                ///
100                /// let client = Client::new("https://api.example.com")?
101                ///     .with_sensitive_header(AUTHORIZATION, "Bearer decafbadcafed00d")?;
102                /// ```
103                pub fn with_sensitive_header<K, V>(self, name: K, value: V) -> Result<Self, crate::error::Error>
104                where
105                    K: TryInto<crate::util::http::HeaderName>,
106                    V: TryInto<crate::util::http::HeaderValue>,
107                    K::Error: Into<crate::util::http::Error>,
108                    V::Error: Into<crate::util::http::Error>,
109                {
110                    let name = name
111                        .try_into()
112                        .map_err(|err| crate::error::Error::BadHeaderName(err.into()))?;
113                    let mut value: ::ploidy_util::http::HeaderValue = value
114                        .try_into()
115                        .map_err(|err| crate::error::Error::BadHeaderValue(name.clone(), err.into()))?;
116                    value.set_sensitive(true);
117                    self.with_header(name, value)
118                }
119
120                pub fn with_user_agent<V>(self, value: V) -> Result<Self, crate::error::Error>
121                where
122                    V: TryInto<crate::util::http::HeaderValue>,
123                    V::Error: Into<crate::util::http::Error>,
124                {
125                    self.with_header(::ploidy_util::http::header::USER_AGENT, value)
126                }
127
128                /// Returns a raw [`RequestBuilder`].
129                ///
130                /// The builder sets the base URL and default headers. Use this for
131                /// requests that the typed client methods don't support.
132                ///
133                /// [`RequestBuilder`]: crate::util::reqwest::RequestBuilder
134                pub fn request(
135                    &self,
136                    method: crate::util::reqwest::Method,
137                    path: &str,
138                ) -> crate::util::reqwest::RequestBuilder {
139                    let mut url = self.base_url.clone();
140                    let _ = url
141                        .path_segments_mut()
142                        .map(|mut segments| {
143                            segments.pop_if_empty().extend(path.split('/'));
144                        });
145                    self.client
146                        .request(method, url)
147                        .headers(self.headers.clone())
148                }
149            }
150
151            #(#mods)*
152        });
153    }
154}
155
156impl IntoCode for CodegenClientModule<'_> {
157    type Code = (&'static str, TokenStream);
158
159    fn into_code(self) -> Self::Code {
160        ("src/client/mod.rs", self.into_token_stream())
161    }
162}