Skip to main content

shape_runtime/stdlib/
helpers.rs

1//! Common argument extraction helpers for stdlib module functions.
2//!
3//! These reduce boilerplate in module implementations by centralising
4//! argument-count validation and typed argument extraction with uniform
5//! error messages.
6
7use shape_value::ValueWord;
8
9/// Validate that `args` has exactly `expected` elements.
10///
11/// Returns `Ok(())` on success, or an error string naming `fn_name` and the
12/// mismatch.
13pub 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
27/// Extract a string argument at `index` from `args`.
28///
29/// Returns the borrowed `&str` on success, or an error string naming
30/// `fn_name` and the position.
31pub 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
46/// Extract a numeric (i64) argument at `index` from `args`.
47///
48/// Returns the `i64` value on success, or an error string naming
49/// `fn_name` and the position.
50pub 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
65/// Extract an f64 argument at `index` from `args`.
66///
67/// Returns the `f64` value on success, or an error string naming
68/// `fn_name` and the position. Accepts both f64 and i64 values (the
69/// latter is widened to f64).
70pub 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
85/// Extract a bool argument at `index` from `args`.
86///
87/// Returns the `bool` value on success, or an error string naming
88/// `fn_name` and the position.
89pub 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
104// ─── String-error context extension ─────────────────────────────────
105
106/// Extension trait that adds `.with_context()` to `Result<T, String>`.
107///
108/// Many stdlib module functions return `Result<T, String>`. This trait
109/// lets callers wrap a bare string error with function-name context:
110///
111/// ```ignore
112/// serde_json::from_str(data)
113///     .map_err(|e| e.to_string())
114///     .with_context("json.parse")?;
115/// // error becomes: "json.parse(): <original message>"
116/// ```
117pub trait StringResultExt<T> {
118    /// Wrap the error string with `"context(): original_error"` on failure.
119    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/// Format a contextualized error string.
130///
131/// Convenience function for call sites that have a non-`String` error and
132/// want to produce `Err(String)` with function-name context in one step:
133///
134/// ```ignore
135/// serde_json::from_str(data)
136///     .map_err(|e| contextualize("json.parse", &e))?;
137/// ```
138#[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}