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