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::{CodegenIdentUsage, ResourceGroup},
9};
10
11/// Generates the `client/mod.rs` source file.
12#[derive(Debug)]
13pub struct CodegenClientModule<'a> {
14    graph: &'a CodegenGraph<'a>,
15    resources: &'a [ResourceGroup<'a>],
16}
17
18impl<'a> CodegenClientModule<'a> {
19    pub fn new(graph: &'a CodegenGraph<'a>, resources: &'a [ResourceGroup<'a>]) -> Self {
20        Self { graph, resources }
21    }
22}
23
24impl ToTokens for CodegenClientModule<'_> {
25    fn to_tokens(&self, tokens: &mut TokenStream) {
26        let client_doc = self.graph.info().label().map(|label| {
27            let doc = match label.version {
28                Some(version) => format!("API client for {} (version {version})", label.title),
29                None => format!("API client for {}", label.title),
30            };
31            quote! { #[doc = #doc] }
32        });
33
34        let mods = ResourceModules(self.resources);
35
36        tokens.append_all(quote! {
37            #client_doc
38            #[derive(Clone, Debug)]
39            pub struct Client {
40                client: ::ploidy_util::reqwest::Client,
41                headers: ::ploidy_util::http::HeaderMap,
42                base_url: ::ploidy_util::url::Url,
43            }
44
45            impl Client {
46                /// Creates a new client.
47                pub fn new(base_url: impl AsRef<str>) -> Result<Self, crate::error::Error> {
48                    Ok(Self::with_reqwest_client(
49                        ::ploidy_util::reqwest::Client::new(),
50                        base_url.as_ref().parse()?,
51                    ))
52                }
53
54                pub fn with_reqwest_client(
55                    client: crate::util::reqwest::Client,
56                    base_url: crate::util::url::Url,
57                ) -> Self {
58                    Self {
59                        client,
60                        headers: ::ploidy_util::http::HeaderMap::new(),
61                        base_url,
62                    }
63                }
64
65                /// Adds a header to each request.
66                pub fn with_header<K, V>(mut self, name: K, value: V) -> Result<Self, crate::error::Error>
67                where
68                    K: TryInto<crate::util::http::HeaderName>,
69                    V: TryInto<crate::util::http::HeaderValue>,
70                    K::Error: Into<crate::util::http::Error>,
71                    V::Error: Into<crate::util::http::Error>,
72                {
73                    let name = name
74                        .try_into()
75                        .map_err(crate::error::Error::bad_header_name)?;
76                    let value = value
77                        .try_into()
78                        .map_err(|err| crate::error::Error::bad_header_value(name.clone(), err))?;
79                    self.headers.insert(name, value);
80                    Ok(Self {
81                        client: self.client,
82                        headers: self.headers,
83                        base_url: self.base_url,
84                    })
85                }
86
87                /// Adds a sensitive header to each request, like a password or a bearer token.
88                /// Sensitive headers won't appear in `Debug` output, and may be treated specially
89                /// by the underlying HTTP stack.
90                ///
91                /// # Example
92                ///
93                /// ```rust,ignore
94                /// use reqwest::header::AUTHORIZATION;
95                ///
96                /// let client = Client::new("https://api.example.com")?
97                ///     .with_sensitive_header(AUTHORIZATION, "Bearer decafbadcafed00d")?;
98                /// ```
99                pub fn with_sensitive_header<K, V>(self, name: K, value: V) -> Result<Self, crate::error::Error>
100                where
101                    K: TryInto<crate::util::http::HeaderName>,
102                    V: TryInto<crate::util::http::HeaderValue>,
103                    K::Error: Into<crate::util::http::Error>,
104                    V::Error: Into<crate::util::http::Error>,
105                {
106                    let name = name
107                        .try_into()
108                        .map_err(crate::error::Error::bad_header_name)?;
109                    let mut value: ::ploidy_util::http::HeaderValue = value
110                        .try_into()
111                        .map_err(|err| crate::error::Error::bad_header_value(name.clone(), err))?;
112                    value.set_sensitive(true);
113                    self.with_header(name, value)
114                }
115
116                pub fn with_user_agent<V>(self, value: V) -> Result<Self, crate::error::Error>
117                where
118                    V: TryInto<crate::util::http::HeaderValue>,
119                    V::Error: Into<crate::util::http::Error>,
120                {
121                    self.with_header(::ploidy_util::http::header::USER_AGENT, value)
122                }
123
124                /// Returns a raw [`RequestBuilder`].
125                ///
126                /// Constructs the request URL by appending `path_and_query`
127                /// to the base URL's path and query. The path can be relative or
128                /// absolute; its segments are appended to the base path.
129                /// Appended query parameters are not deduplicated.
130                ///
131                /// For example, if this client's base URL is
132                /// `https://api.example.com/v1` and `path_and_query` is
133                /// `/pets/list?limit=10`, the request URL is
134                /// `https://api.example.com/v1/pets/list?limit=10`.
135                /// Prefer using the builder's [`query`] method to append
136                /// dynamic query parameters; use `path_and_query` for static
137                /// parameters.
138                ///
139                /// The request includes the client's default headers.
140                ///
141                /// Use this for requests that the client's operation methods
142                /// don't cover.
143                ///
144                /// [`RequestBuilder`]: crate::util::reqwest::RequestBuilder
145                /// [`query`]: crate::util::reqwest::RequestBuilder::query
146                pub fn request(
147                    &self,
148                    method: crate::util::reqwest::Method,
149                    path_and_query: &str,
150                ) -> Result<crate::util::reqwest::RequestBuilder, crate::error::Error> {
151                    let url = ::ploidy_util::url::UrlExt::with_path_and_query(
152                        self.base_url.clone(),
153                        path_and_query,
154                    )?;
155                    Ok(self.client
156                        .request(method, url)
157                        .headers(self.headers.clone()))
158                }
159            }
160
161            #mods
162        });
163    }
164}
165
166impl IntoCode for CodegenClientModule<'_> {
167    type Code = (&'static str, TokenStream);
168
169    fn into_code(self) -> Self::Code {
170        ("src/client/mod.rs", self.into_token_stream())
171    }
172}
173
174#[derive(Debug)]
175struct ResourceModules<'a>(&'a [ResourceGroup<'a>]);
176
177impl ToTokens for ResourceModules<'_> {
178    fn to_tokens(&self, tokens: &mut TokenStream) {
179        tokens.append_all(self.0.iter().map(|ident| match ident {
180            &ResourceGroup::Named(name) => {
181                let cfg = CfgFeature::Single(name);
182                let mod_name = CodegenIdentUsage::Module(name);
183                quote! {
184                    #cfg
185                    pub mod #mod_name;
186                }
187            }
188            ResourceGroup::Default => quote!(
189                pub mod default;
190            ),
191        }));
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    use ploidy_core::arena::Arena;
200    use pretty_assertions::assert_eq;
201    use syn::parse_quote;
202
203    use crate::naming::UniqueIdents;
204
205    #[test]
206    fn test_resource_modules_gates_named_resources_and_keeps_default_ungated() {
207        let arena = Arena::new();
208        let mut scope = UniqueIdents::new(&arena);
209        let resources = [
210            ResourceGroup::Default,
211            ResourceGroup::Named(scope.claim("customer_profiles")),
212        ];
213        let modules = ResourceModules(&resources);
214
215        let actual: syn::File = parse_quote!(#modules);
216        let expected: syn::File = parse_quote! {
217            pub mod default;
218
219            #[cfg(feature = "customer-profiles")]
220            pub mod customer_profiles;
221        };
222        assert_eq!(actual, expected);
223    }
224}