polydat_nodes/
param_helpers.rs1use regex::Regex;
23
24#[polydat::polydat_node(category = Arithmetic)]
36fn required(
37 input: Option<u64>,
38 #[poly_default("value")] name: polydat::derive_support::Const<&str>,
39) -> u64 {
40 input.unwrap_or_else(|| panic!("required({}): value was not defined", name.0))
41}
42
43#[polydat::polydat_node(category = Arithmetic)]
55fn this_or(primary: Option<u64>, default: u64) -> u64 {
56 primary.unwrap_or(default)
57}
58
59#[polydat::polydat_node(category = Arithmetic)]
69fn is_positive(
70 input: u64,
71 #[poly_default("value")] name: polydat::derive_support::Const<&str>,
72) -> u64 {
73 if input == 0 {
74 panic!("is_positive({}): value must be > 0, got 0", name.0);
75 }
76 input
77}
78
79#[polydat::polydat_node(category = Arithmetic, validate = in_range_validate)]
89fn in_range(
90 input: u64,
91 #[poly_default(0u64)] lo: polydat::derive_support::Const<u64>,
92 #[poly_default(u64::MAX)] hi: polydat::derive_support::Const<u64>,
93) -> u64 {
94 if input < *lo || input > *hi {
95 panic!("in_range: value {input} outside [{}, {}]", *lo, *hi);
96 }
97 input
98}
99
100fn in_range_validate(
103 _name: &str,
104 consts: &[polydat::dsl::factory::ConstArg],
105) -> Result<(), String> {
106 let lo = consts.first().map(|c| c.as_u64()).unwrap_or(0);
107 let hi = consts.get(1).map(|c| c.as_u64()).unwrap_or(u64::MAX);
108 if lo > hi {
109 return Err(format!("lo ({lo}) must be <= hi ({hi})"));
110 }
111 Ok(())
112}
113
114#[polydat::polydat_node(category = Arithmetic, validate = is_one_of_validate)]
125fn is_one_of(input: u64, allowed: polydat::derive_support::Const<&[u64]>) -> u64 {
126 if !allowed.contains(&input) {
127 panic!(
128 "is_one_of: value {input} not in allowed set {:?}",
129 allowed.0
130 );
131 }
132 input
133}
134
135fn is_one_of_validate(
139 _name: &str,
140 consts: &[polydat::dsl::factory::ConstArg],
141) -> Result<(), String> {
142 if consts.is_empty() {
143 return Err("at least one allowed value required".into());
144 }
145 Ok(())
146}
147
148fn compile_matches_regex(pattern: &str) -> Regex {
154 Regex::new(pattern).unwrap_or_else(|e| panic!("matches: invalid regex {pattern:?}: {e}"))
155}
156
157#[polydat::polydat_node(category = Arithmetic)]
160fn matches(
161 input: &str,
162 pattern: polydat::derive_support::Const<&str>,
163 #[poly_const(compile_matches_regex, from = pattern)] re: &Regex,
164) -> String {
165 if !re.is_match(input) {
166 panic!(
167 "matches: value {input:?} does not match pattern {:?}",
168 pattern.0
169 );
170 }
171 input.to_string()
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use polydat::ast::{PolydatNode, Value};
178
179 #[test]
180 fn required_passes_defined_value() {
181 let n = Required::new("x".to_string());
182 let mut out = [Value::None];
183 n.eval(&[Value::U64(42)], &mut out);
184 assert_eq!(out[0].as_u64(), 42);
185 }
186
187 #[test]
188 #[should_panic(expected = "required(x): value was not defined")]
189 fn required_panics_on_none() {
190 let n = Required::new("x".to_string());
191 let mut out = [Value::None];
192 n.eval(&[Value::None], &mut out);
193 }
194
195 #[test]
196 fn this_or_prefers_primary_when_defined() {
197 let n = ThisOr::new();
198 let mut out = [Value::None];
199 n.eval(&[Value::U64(7), Value::U64(99)], &mut out);
200 assert_eq!(out[0].as_u64(), 7);
201 }
202
203 #[test]
204 fn this_or_falls_back_to_default_on_none() {
205 let n = ThisOr::new();
206 let mut out = [Value::None];
207 n.eval(&[Value::None, Value::U64(99)], &mut out);
208 assert_eq!(out[0].as_u64(), 99);
209 }
210
211 #[test]
212 fn is_positive_passes_positive() {
213 let n = IsPositive::new("rate".to_string());
214 let mut out = [Value::None];
215 n.eval(&[Value::U64(1)], &mut out);
216 assert_eq!(out[0].as_u64(), 1);
217 }
218
219 #[test]
220 #[should_panic(expected = "is_positive(rate)")]
221 fn is_positive_panics_on_zero() {
222 let n = IsPositive::new("rate".to_string());
223 let mut out = [Value::None];
224 n.eval(&[Value::U64(0)], &mut out);
225 }
226
227 #[test]
228 fn in_range_passes_interior() {
229 let n = InRange::new(10, 100);
230 let mut out = [Value::None];
231 n.eval(&[Value::U64(50)], &mut out);
232 assert_eq!(out[0].as_u64(), 50);
233 n.eval(&[Value::U64(10)], &mut out);
234 assert_eq!(out[0].as_u64(), 10);
235 n.eval(&[Value::U64(100)], &mut out);
236 assert_eq!(out[0].as_u64(), 100);
237 }
238
239 #[test]
240 #[should_panic(expected = "outside [10, 100]")]
241 fn in_range_panics_below() {
242 let n = InRange::new(10, 100);
243 let mut out = [Value::None];
244 n.eval(&[Value::U64(5)], &mut out);
245 }
246
247 #[test]
248 #[should_panic(expected = "outside [10, 100]")]
249 fn in_range_panics_above() {
250 let n = InRange::new(10, 100);
251 let mut out = [Value::None];
252 n.eval(&[Value::U64(101)], &mut out);
253 }
254
255 #[test]
256 fn is_one_of_passes_allowed() {
257 let n = IsOneOf::new(vec![1, 2, 3, 5, 8]);
258 let mut out = [Value::None];
259 n.eval(&[Value::U64(5)], &mut out);
260 assert_eq!(out[0].as_u64(), 5);
261 }
262
263 #[test]
264 #[should_panic(expected = "not in allowed set")]
265 fn is_one_of_panics_on_disallowed() {
266 let n = IsOneOf::new(vec![1, 2, 3]);
267 let mut out = [Value::None];
268 n.eval(&[Value::U64(4)], &mut out);
269 }
270
271 #[test]
272 fn matches_passes_matching_string() {
273 let n = Matches::new(r"^\w+@\w+\.\w+$".to_string());
274 let mut out = [Value::None];
275 n.eval(&[Value::Str("jshook@example.com".into())], &mut out);
276 assert_eq!(out[0].as_str(), "jshook@example.com");
277 }
278
279 #[test]
280 #[should_panic(expected = "does not match pattern")]
281 fn matches_panics_on_mismatch() {
282 let n = Matches::new(r"^\d+$".to_string());
283 let mut out = [Value::None];
284 n.eval(&[Value::Str("abc".into())], &mut out);
285 }
286}