Skip to main content

nifi_rust_client/
builder.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use snafu::ResultExt as _;
5use url::Url;
6
7use crate::NifiClient;
8use crate::NifiError;
9use crate::config::credentials::CredentialProvider;
10use crate::error::{HttpSnafu, InvalidBaseUrlSnafu, InvalidCertificateSnafu};
11
12/// Builder for [`NifiClient`].
13///
14/// Use this when you need to configure timeouts, proxies, or TLS options beyond
15/// the defaults provided by the convenience constructors.
16///
17/// # Example
18///
19/// ```no_run
20/// use std::time::Duration;
21/// use nifi_rust_client::NifiClientBuilder;
22/// use url::Url;
23///
24/// # async fn example() -> Result<(), nifi_rust_client::NifiError> {
25/// let client = NifiClientBuilder::new("https://nifi.example.com:8443")?
26///     .timeout(Duration::from_secs(60))
27///     .connect_timeout(Duration::from_secs(10))
28///     .proxy(Url::parse("http://proxy.internal:3128").unwrap())
29///     .build()?;
30/// # Ok(())
31/// # }
32/// ```
33pub struct NifiClientBuilder {
34    base_url: Url,
35    timeout: Option<Duration>,
36    connect_timeout: Option<Duration>,
37    proxy_all: Option<Url>,
38    proxy_http: Option<Url>,
39    proxy_https: Option<Url>,
40    danger_accept_invalid_certs: bool,
41    root_certificates: Vec<Vec<u8>>,
42    credential_provider: Option<Arc<dyn CredentialProvider>>,
43    retry_policy: Option<crate::config::retry::RetryPolicy>,
44    #[cfg(feature = "dynamic")]
45    version_strategy: Option<crate::dynamic::VersionResolutionStrategy>,
46}
47
48impl std::fmt::Debug for NifiClientBuilder {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        let mut s = f.debug_struct("NifiClientBuilder");
51        s.field("base_url", &self.base_url)
52            .field("timeout", &self.timeout)
53            .field("connect_timeout", &self.connect_timeout)
54            .field("proxy_all", &self.proxy_all)
55            .field("proxy_http", &self.proxy_http)
56            .field("proxy_https", &self.proxy_https)
57            .field(
58                "danger_accept_invalid_certs",
59                &self.danger_accept_invalid_certs,
60            )
61            .field(
62                "root_certificates",
63                &format!("[{} certs]", self.root_certificates.len()),
64            )
65            .field(
66                "credential_provider",
67                &self.credential_provider.as_ref().map(|c| format!("{c:?}")),
68            )
69            .field("retry_policy", &self.retry_policy);
70        #[cfg(feature = "dynamic")]
71        s.field("version_strategy", &self.version_strategy);
72        s.finish()
73    }
74}
75
76impl NifiClientBuilder {
77    /// Create a new builder targeting the given NiFi base URL.
78    ///
79    /// Returns an error if `base_url` cannot be parsed.
80    pub fn new(base_url: &str) -> Result<Self, NifiError> {
81        let base_url = Url::parse(base_url).context(InvalidBaseUrlSnafu)?;
82        Ok(Self {
83            base_url,
84            timeout: None,
85            connect_timeout: None,
86            proxy_all: None,
87            proxy_http: None,
88            proxy_https: None,
89            danger_accept_invalid_certs: false,
90            root_certificates: Vec::new(),
91            credential_provider: None,
92            retry_policy: None,
93            #[cfg(feature = "dynamic")]
94            version_strategy: None,
95        })
96    }
97
98    /// Set the total request timeout.
99    ///
100    /// The timeout applies from when the request starts connecting until the
101    /// response body is fully received.
102    pub fn timeout(mut self, duration: Duration) -> Self {
103        self.timeout = Some(duration);
104        self
105    }
106
107    /// Set the TCP connection timeout.
108    pub fn connect_timeout(mut self, duration: Duration) -> Self {
109        self.connect_timeout = Some(duration);
110        self
111    }
112
113    /// Route all traffic (HTTP and HTTPS) through the given proxy.
114    pub fn proxy(mut self, url: Url) -> Self {
115        self.proxy_all = Some(url);
116        self
117    }
118
119    /// Route HTTP traffic through the given proxy.
120    pub fn http_proxy(mut self, url: Url) -> Self {
121        self.proxy_http = Some(url);
122        self
123    }
124
125    /// Route HTTPS traffic through the given proxy.
126    pub fn https_proxy(mut self, url: Url) -> Self {
127        self.proxy_https = Some(url);
128        self
129    }
130
131    /// Skip TLS certificate verification.
132    ///
133    /// Only use this in development against self-signed certificates.
134    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
135        self.danger_accept_invalid_certs = accept;
136        self
137    }
138
139    /// Trust an additional PEM-encoded CA certificate.
140    ///
141    /// May be called multiple times to add more than one certificate.
142    pub fn add_root_certificate(mut self, pem: &[u8]) -> Self {
143        self.root_certificates.push(pem.to_vec());
144        self
145    }
146
147    /// Configure a [`CredentialProvider`] for automatic token refresh.
148    ///
149    /// When set, the client will automatically re-authenticate on 401 responses
150    /// by calling the provider to obtain fresh credentials and then re-issuing
151    /// the failed request.
152    pub fn credential_provider(mut self, provider: impl CredentialProvider + 'static) -> Self {
153        self.credential_provider = Some(Arc::new(provider));
154        self
155    }
156
157    /// Configure a [`RetryPolicy`](crate::config::retry::RetryPolicy) for transient error retry.
158    ///
159    /// When set, HTTP helpers automatically retry
160    /// [retryable](crate::NifiError::is_retryable) errors using exponential backoff.
161    pub fn retry_policy(mut self, policy: crate::config::retry::RetryPolicy) -> Self {
162        self.retry_policy = Some(policy);
163        self
164    }
165
166    /// Configure a [`VersionResolutionStrategy`](crate::dynamic::VersionResolutionStrategy)
167    /// for the dynamic client.
168    ///
169    /// Controls how the client resolves a detected NiFi version to a supported
170    /// client version. Default is `Strict`.
171    #[cfg(feature = "dynamic")]
172    pub fn version_strategy(mut self, strategy: crate::dynamic::VersionResolutionStrategy) -> Self {
173        self.version_strategy = Some(strategy);
174        self
175    }
176
177    /// Build the [`NifiClient`].
178    pub fn build(self) -> Result<NifiClient, NifiError> {
179        let mut builder = reqwest::Client::builder()
180            .danger_accept_invalid_certs(self.danger_accept_invalid_certs);
181
182        if let Some(d) = self.timeout {
183            builder = builder.timeout(d);
184        }
185        if let Some(d) = self.connect_timeout {
186            builder = builder.connect_timeout(d);
187        }
188
189        for pem in &self.root_certificates {
190            let cert = reqwest::Certificate::from_pem(pem).context(InvalidCertificateSnafu)?;
191            builder = builder.add_root_certificate(cert);
192        }
193
194        if let Some(url) = self.proxy_all {
195            let proxy = reqwest::Proxy::all(url.as_str()).context(HttpSnafu)?;
196            builder = builder.proxy(proxy);
197        }
198        if let Some(url) = self.proxy_http {
199            let proxy = reqwest::Proxy::http(url.as_str()).context(HttpSnafu)?;
200            builder = builder.proxy(proxy);
201        }
202        if let Some(url) = self.proxy_https {
203            let proxy = reqwest::Proxy::https(url.as_str()).context(HttpSnafu)?;
204            builder = builder.proxy(proxy);
205        }
206
207        let http = builder.build().context(HttpSnafu)?;
208        Ok(NifiClient::from_parts(
209            self.base_url,
210            http,
211            self.credential_provider,
212            self.retry_policy,
213        ))
214    }
215
216    /// Build a [`DynamicClient`](crate::dynamic::DynamicClient) that auto-detects the NiFi version.
217    ///
218    /// Version detection happens lazily — either when `login()` is called
219    /// (recommended) or when `detect_version()` is called explicitly.
220    ///
221    /// Uses the configured [`VersionResolutionStrategy`](crate::dynamic::VersionResolutionStrategy)
222    /// (default: `Strict`). Set via [`.version_strategy()`](Self::version_strategy).
223    ///
224    /// # Example
225    ///
226    /// ```no_run
227    /// # async fn example() -> Result<(), nifi_rust_client::NifiError> {
228    /// use nifi_rust_client::NifiClientBuilder;
229    /// use nifi_rust_client::dynamic::VersionResolutionStrategy;
230    ///
231    /// let client = NifiClientBuilder::new("https://nifi.example.com:8443")?
232    ///     .danger_accept_invalid_certs(true)
233    ///     .version_strategy(VersionResolutionStrategy::Closest)
234    ///     .build_dynamic()?;
235    ///
236    /// // login() authenticates AND detects the NiFi version automatically.
237    /// client.login("admin", "password").await?;
238    /// # Ok(())
239    /// # }
240    /// ```
241    #[cfg(feature = "dynamic")]
242    pub fn build_dynamic(self) -> Result<crate::dynamic::DynamicClient, NifiError> {
243        let strategy = self.version_strategy.unwrap_or_default();
244        let client = self.build()?;
245        Ok(crate::dynamic::DynamicClient::with_strategy(
246            client, strategy,
247        ))
248    }
249}