Skip to main content

nifi_rust_client/
builder.rs

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