Skip to main content

nifi_rust_client/
require.rs

1//! Ergonomic extraction of required fields from `Option<T>` values.
2//!
3//! Dynamic-mode NiFi DTOs carry every field as `Option<T>` because the
4//! union of fields across supported versions includes values that may or
5//! may not be populated by any given NiFi server. Writing
6//! `.ok_or_else(|| ...)` at every hop is tedious and loses the "which
7//! field was absent" signal callers need for graceful fallback.
8//!
9//! This module provides two helpers:
10//!
11//! - [`RequireField::require`] — a blanket trait method on any `Option<T>`
12//!   that returns `Result<&T, NifiError>`.
13//! - The [`require!`](crate::require) macro — sugar for walking a chain
14//!   of fields, stamping a dotted path into the error automatically.
15//!
16//! Both work in static and dynamic mode; the helper is not mode-gated.
17
18use crate::error::NifiError;
19
20/// Extension trait for `Option<T>` that returns
21/// [`NifiError::MissingField`] instead of panicking or forcing an
22/// `.ok_or_else` closure.
23///
24/// # Example
25///
26/// ```
27/// use nifi_rust_client::{NifiError, RequireField};
28///
29/// fn example() -> Result<(), NifiError> {
30///     let version: Option<String> = Some("2.9.0".to_string());
31///     let v: &String = version.require("version")?;
32///     assert_eq!(v, "2.9.0");
33///     Ok(())
34/// }
35/// # example().unwrap();
36/// ```
37pub trait RequireField<T> {
38    /// Borrow the inner value, returning [`NifiError::MissingField`] if absent.
39    ///
40    /// `path` identifies the field in the returned error. Prefer the
41    /// [`require!`](crate::require) macro when walking a chain of fields —
42    /// it fills in the path automatically.
43    fn require(&self, path: &str) -> Result<&T, NifiError>;
44}
45
46impl<T> RequireField<T> for Option<T> {
47    fn require(&self, path: &str) -> Result<&T, NifiError> {
48        self.as_ref().ok_or_else(|| NifiError::MissingField {
49            path: path.to_owned(),
50        })
51    }
52}
53
54/// Walk a chain of `Option<T>` fields, returning a borrow of the leaf value.
55///
56/// Expands into nested [`RequireField::require`] calls, stamping each hop's
57/// identifier into a dotted path that is carried in the
58/// [`NifiError::MissingField`] error on failure.
59///
60/// # Example
61///
62/// ```
63/// use nifi_rust_client::{NifiError, RequireField, require};
64///
65/// struct Outer { inner: Option<Inner> }
66/// struct Inner { leaf: Option<String> }
67///
68/// fn example() -> Result<(), NifiError> {
69///     let o = Outer { inner: Some(Inner { leaf: Some("v".to_string()) }) };
70///     let leaf: &String = require!(o.inner.leaf);
71///     assert_eq!(leaf, "v");
72///
73///     let missing = Outer { inner: Some(Inner { leaf: None }) };
74///     let err = (|| -> Result<(), NifiError> {
75///         let _leaf: &String = require!(missing.inner.leaf);
76///         Ok(())
77///     })().unwrap_err();
78///     assert!(matches!(err, NifiError::MissingField { path } if path == "inner.leaf"));
79///     Ok(())
80/// }
81/// # example().unwrap();
82/// ```
83///
84/// # Notes
85///
86/// The macro expands into an expression block that uses the `?` operator,
87/// so the surrounding function must return
88/// `Result<_, impl From<NifiError>>`. To use it inside a function that
89/// returns something else, wrap the call in a closure that returns
90/// `Result<_, NifiError>`, as shown above.
91#[macro_export]
92macro_rules! require {
93    ($root:ident $(. $field:ident)+) => {{
94        let mut __path = ::std::string::String::new();
95        let __v = &$root;
96        $(
97            if !__path.is_empty() {
98                __path.push('.');
99            }
100            __path.push_str(::core::stringify!($field));
101            let __v = $crate::RequireField::require(&__v.$field, &__path)?;
102        )+
103        __v
104    }};
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn require_some_returns_borrow() {
113        let opt: Option<String> = Some("v1".to_string());
114        let got: &String = opt.require("version").unwrap();
115        assert_eq!(got, "v1");
116    }
117
118    #[test]
119    fn require_none_returns_missing_field_with_verbatim_path() {
120        let opt: Option<String> = None;
121        let err = opt.require("about.version").unwrap_err();
122        match err {
123            NifiError::MissingField { path } => assert_eq!(path, "about.version"),
124            other => panic!("expected MissingField, got {other:?}"),
125        }
126    }
127
128    #[test]
129    fn require_does_not_consume_the_option() {
130        let opt: Option<String> = Some("kept".to_string());
131        let _first = opt.require("x").unwrap();
132        let second = opt.require("x").unwrap();
133        assert_eq!(second, "kept");
134    }
135
136    // ── Macro tests ──────────────────────────────────────────────────────
137    //
138    // Macro tests use locally-defined structs to avoid depending on any
139    // generated dynamic DTO. This keeps the suite stable across NiFi spec
140    // bumps.
141
142    #[derive(Debug)]
143    struct Outer {
144        inner: Option<Inner>,
145    }
146    #[derive(Debug)]
147    struct Inner {
148        leaf: Option<String>,
149    }
150
151    #[test]
152    fn macro_single_hop_ok() {
153        let o = Outer {
154            inner: Some(Inner {
155                leaf: Some("v".to_string()),
156            }),
157        };
158        let result: Result<String, NifiError> = (|| {
159            let got: &String = crate::require!(o.inner.leaf);
160            Ok(got.clone())
161        })();
162        assert_eq!(result.unwrap(), "v");
163    }
164
165    #[test]
166    fn macro_missing_outer_reports_outer_path() {
167        let o = Outer { inner: None };
168        let result: Result<(), NifiError> = (|| {
169            let _leaf: &String = crate::require!(o.inner.leaf);
170            Ok(())
171        })();
172        let err = result.unwrap_err();
173        match err {
174            NifiError::MissingField { path } => assert_eq!(path, "inner"),
175            other => panic!("expected MissingField, got {other:?}"),
176        }
177    }
178
179    #[test]
180    fn macro_missing_leaf_reports_full_dotted_path() {
181        let o = Outer {
182            inner: Some(Inner { leaf: None }),
183        };
184        let result: Result<(), NifiError> = (|| {
185            let _leaf: &String = crate::require!(o.inner.leaf);
186            Ok(())
187        })();
188        let err = result.unwrap_err();
189        match err {
190            NifiError::MissingField { path } => assert_eq!(path, "inner.leaf"),
191            other => panic!("expected MissingField, got {other:?}"),
192        }
193    }
194}