shape_runtime/stdlib/
helpers.rs1use shape_value::ValueWord;
8
9pub fn check_arg_count(args: &[ValueWord], expected: usize, fn_name: &str) -> Result<(), String> {
14 if args.len() != expected {
15 Err(format!(
16 "{}() expected {} argument{}, got {}",
17 fn_name,
18 expected,
19 if expected == 1 { "" } else { "s" },
20 args.len()
21 ))
22 } else {
23 Ok(())
24 }
25}
26
27pub fn extract_string_arg<'a>(
32 args: &'a [ValueWord],
33 index: usize,
34 fn_name: &str,
35) -> Result<&'a str, String> {
36 args.get(index)
37 .and_then(|a| a.as_str())
38 .ok_or_else(|| {
39 format!(
40 "{}() requires a string argument at position {}",
41 fn_name, index
42 )
43 })
44}
45
46pub fn extract_number_arg(
51 args: &[ValueWord],
52 index: usize,
53 fn_name: &str,
54) -> Result<i64, String> {
55 args.get(index)
56 .and_then(|a| a.as_i64())
57 .ok_or_else(|| {
58 format!(
59 "{}() requires a numeric argument at position {}",
60 fn_name, index
61 )
62 })
63}
64
65pub fn extract_float_arg(
71 args: &[ValueWord],
72 index: usize,
73 fn_name: &str,
74) -> Result<f64, String> {
75 args.get(index)
76 .and_then(|a| a.as_f64().or_else(|| a.as_i64().map(|i| i as f64)))
77 .ok_or_else(|| {
78 format!(
79 "{}() requires a numeric argument at position {}",
80 fn_name, index
81 )
82 })
83}
84
85pub fn extract_bool_arg(
90 args: &[ValueWord],
91 index: usize,
92 fn_name: &str,
93) -> Result<bool, String> {
94 args.get(index)
95 .and_then(|a| a.as_bool())
96 .ok_or_else(|| {
97 format!(
98 "{}() requires a bool argument at position {}",
99 fn_name, index
100 )
101 })
102}
103
104pub trait StringResultExt<T> {
118 fn with_context(self, context: &str) -> Result<T, String>;
120}
121
122impl<T> StringResultExt<T> for Result<T, String> {
123 #[inline]
124 fn with_context(self, context: &str) -> Result<T, String> {
125 self.map_err(|e| format!("{}(): {}", context, e))
126 }
127}
128
129#[inline]
139pub fn contextualize(context: &str, err: &dyn std::fmt::Display) -> String {
140 format!("{}(): {}", context, err)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use std::sync::Arc;
147
148 #[test]
149 fn check_arg_count_exact() {
150 let args = vec![ValueWord::from_i64(1), ValueWord::from_i64(2)];
151 assert!(check_arg_count(&args, 2, "test_fn").is_ok());
152 }
153
154 #[test]
155 fn check_arg_count_mismatch() {
156 let args = vec![ValueWord::from_i64(1)];
157 let err = check_arg_count(&args, 2, "test_fn").unwrap_err();
158 assert!(err.contains("test_fn()"));
159 assert!(err.contains("expected 2 arguments"));
160 assert!(err.contains("got 1"));
161 }
162
163 #[test]
164 fn check_arg_count_singular() {
165 let args = vec![];
166 let err = check_arg_count(&args, 1, "foo").unwrap_err();
167 assert!(err.contains("expected 1 argument,"));
168 }
169
170 #[test]
171 fn extract_string_arg_success() {
172 let args = vec![ValueWord::from_string(Arc::new("hello".to_string()))];
173 assert_eq!(extract_string_arg(&args, 0, "fn").unwrap(), "hello");
174 }
175
176 #[test]
177 fn extract_string_arg_wrong_type() {
178 let args = vec![ValueWord::from_i64(42)];
179 let err = extract_string_arg(&args, 0, "fn").unwrap_err();
180 assert!(err.contains("string argument at position 0"));
181 }
182
183 #[test]
184 fn extract_string_arg_out_of_bounds() {
185 let args: Vec<ValueWord> = vec![];
186 assert!(extract_string_arg(&args, 0, "fn").is_err());
187 }
188
189 #[test]
190 fn extract_number_arg_success() {
191 let args = vec![ValueWord::from_i64(99)];
192 assert_eq!(extract_number_arg(&args, 0, "fn").unwrap(), 99);
193 }
194
195 #[test]
196 fn extract_number_arg_wrong_type() {
197 let args = vec![ValueWord::from_string(Arc::new("nope".to_string()))];
198 let err = extract_number_arg(&args, 0, "fn").unwrap_err();
199 assert!(err.contains("numeric argument at position 0"));
200 }
201
202 #[test]
203 fn extract_number_arg_out_of_bounds() {
204 let args: Vec<ValueWord> = vec![];
205 assert!(extract_number_arg(&args, 0, "fn").is_err());
206 }
207
208 #[test]
209 fn extract_float_arg_from_f64() {
210 let args = vec![ValueWord::from_f64(3.14)];
211 let val = extract_float_arg(&args, 0, "test").unwrap();
212 assert!((val - 3.14).abs() < f64::EPSILON);
213 }
214
215 #[test]
216 fn extract_float_arg_from_i64() {
217 let args = vec![ValueWord::from_i64(42)];
218 let val = extract_float_arg(&args, 0, "test").unwrap();
219 assert!((val - 42.0).abs() < f64::EPSILON);
220 }
221
222 #[test]
223 fn extract_float_arg_wrong_type() {
224 let args = vec![ValueWord::from_string(Arc::new("nope".to_string()))];
225 let err = extract_float_arg(&args, 0, "fn").unwrap_err();
226 assert!(err.contains("numeric argument at position 0"));
227 }
228
229 #[test]
230 fn extract_bool_arg_success() {
231 let args = vec![ValueWord::from_bool(true)];
232 assert!(extract_bool_arg(&args, 0, "fn").unwrap());
233 }
234
235 #[test]
236 fn extract_bool_arg_wrong_type() {
237 let args = vec![ValueWord::from_i64(1)];
238 let err = extract_bool_arg(&args, 0, "fn").unwrap_err();
239 assert!(err.contains("bool argument at position 0"));
240 }
241
242 #[test]
243 fn string_result_with_context() {
244 let result: Result<i32, String> = Err("file not found".to_string());
245 let err = result.with_context("file.read").unwrap_err();
246 assert_eq!(err, "file.read(): file not found");
247 }
248
249 #[test]
250 fn string_result_with_context_ok() {
251 let result: Result<i32, String> = Ok(42);
252 assert_eq!(result.with_context("file.read").unwrap(), 42);
253 }
254
255 #[test]
256 fn contextualize_formats_correctly() {
257 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
258 let msg = contextualize("file.read", &io_err);
259 assert!(msg.starts_with("file.read(): "));
260 assert!(msg.contains("gone"));
261 }
262}