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}