1use crate::types::ToolCallResult;
28use serde_json::Value;
29
30pub type ArgResult<T> = Result<Option<T>, String>;
33
34macro_rules! try_arg {
37 ($expr:expr) => {
38 match $expr {
39 Ok(v) => v,
40 Err(msg) => return crate::types::ToolCallResult::error(msg),
41 }
42 };
43}
44pub(crate) use try_arg;
45
46fn type_error(name: &str, expected: &str, value: &Value) -> String {
47 format!("Invalid {name}: expected {expected}, got {value}")
48}
49
50fn lookup<'a>(args: &'a Value, name: &str) -> Option<&'a Value> {
52 args.get(name).filter(|v| !v.is_null())
53}
54
55pub fn opt_u64(args: &Value, name: &str) -> ArgResult<u64> {
64 let Some(value) = lookup(args, name) else {
65 return Ok(None);
66 };
67 if let Some(n) = value.as_u64() {
68 return Ok(Some(n));
69 }
70 if let Some(f) = value.as_f64() {
71 if f.is_finite() && f >= 0.0 && f.fract() == 0.0 {
72 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
73 return Ok(Some(f as u64));
74 }
75 }
76 if let Some(s) = value.as_str() {
77 if let Ok(n) = s.trim().parse::<u64>() {
78 return Ok(Some(n));
79 }
80 }
81 Err(type_error(name, "integer", value))
82}
83
84pub fn opt_f64(args: &Value, name: &str) -> ArgResult<f64> {
93 let Some(value) = lookup(args, name) else {
94 return Ok(None);
95 };
96 if let Some(f) = value.as_f64() {
97 return Ok(Some(f));
98 }
99 if let Some(s) = value.as_str() {
100 if let Ok(f) = s.trim().parse::<f64>() {
101 if f.is_finite() {
102 return Ok(Some(f));
103 }
104 }
105 }
106 Err(type_error(name, "number", value))
107}
108
109pub fn opt_bool(args: &Value, name: &str) -> ArgResult<bool> {
118 let Some(value) = lookup(args, name) else {
119 return Ok(None);
120 };
121 if let Some(b) = value.as_bool() {
122 return Ok(Some(b));
123 }
124 if let Some(s) = value.as_str() {
125 let t = s.trim();
126 if t.eq_ignore_ascii_case("true") {
127 return Ok(Some(true));
128 }
129 if t.eq_ignore_ascii_case("false") {
130 return Ok(Some(false));
131 }
132 }
133 Err(type_error(name, "boolean", value))
134}
135
136pub fn opt_str<'a>(args: &'a Value, name: &str) -> ArgResult<&'a str> {
145 let Some(value) = lookup(args, name) else {
146 return Ok(None);
147 };
148 match value.as_str() {
149 Some(s) => Ok(Some(s)),
150 None => Err(type_error(name, "string", value)),
151 }
152}
153
154pub fn required_str<'a>(args: &'a Value, name: &str) -> Result<&'a str, String> {
161 match lookup(args, name) {
162 None => Err(format!("Missing required argument: {name}")),
163 Some(v) => match v.as_str() {
164 Some(s) => Ok(s),
165 None => Err(type_error(name, "string", v)),
166 },
167 }
168}
169
170#[cfg(test)]
171#[allow(clippy::disallowed_methods)] mod tests {
173 use super::*;
174 use serde_json::json;
175
176 #[test]
177 fn absent_and_null_are_none_not_errors() {
178 let args = json!({ "max_tokens": null });
179 assert_eq!(opt_u64(&args, "max_tokens"), Ok(None));
180 assert_eq!(opt_u64(&args, "iterations"), Ok(None));
181 assert_eq!(opt_f64(&args, "assert_tps"), Ok(None));
182 assert_eq!(opt_bool(&args, "stats"), Ok(None));
183 assert_eq!(opt_str(&args, "prompt"), Ok(None));
184 }
185
186 #[test]
187 fn correctly_typed_values_pass_through() {
188 let args = json!({
189 "max_tokens": 8,
190 "assert_tps": 100_000.0,
191 "stats": true,
192 "filter": "attn_qkv",
193 });
194 assert_eq!(opt_u64(&args, "max_tokens"), Ok(Some(8)));
195 assert_eq!(opt_f64(&args, "assert_tps"), Ok(Some(100_000.0)));
196 assert_eq!(opt_bool(&args, "stats"), Ok(Some(true)));
197 assert_eq!(opt_str(&args, "filter"), Ok(Some("attn_qkv")));
198 }
199
200 #[test]
203 fn numeric_strings_are_coerced_not_dropped() {
204 let args = json!({ "max_tokens": "8", "assert_tps": "100000", "stats": "true" });
205 assert_eq!(opt_u64(&args, "max_tokens"), Ok(Some(8)));
206 assert_eq!(opt_f64(&args, "assert_tps"), Ok(Some(100_000.0)));
207 assert_eq!(opt_bool(&args, "stats"), Ok(Some(true)));
208 }
209
210 #[test]
211 fn integral_float_is_accepted_for_integer() {
212 assert_eq!(opt_u64(&json!({ "n": 8.0 }), "n"), Ok(Some(8)));
213 }
214
215 #[test]
216 fn unusable_values_are_errors_not_silent_drops() {
217 assert!(opt_u64(&json!({ "n": "eight" }), "n").is_err());
218 assert!(opt_u64(&json!({ "n": -1 }), "n").is_err());
219 assert!(opt_u64(&json!({ "n": 1.5 }), "n").is_err());
220 assert!(opt_u64(&json!({ "n": true }), "n").is_err());
221 assert!(opt_f64(&json!({ "n": "fast" }), "n").is_err());
222 assert!(opt_bool(&json!({ "n": 1 }), "n").is_err());
223 assert!(opt_bool(&json!({ "n": "yes" }), "n").is_err());
224 assert!(opt_str(&json!({ "n": 42 }), "n").is_err());
225 }
226
227 #[test]
228 fn error_message_names_the_field_the_type_and_the_value() {
229 let err = opt_u64(&json!({ "max_tokens": "eight" }), "max_tokens")
230 .expect_err("string 'eight' is not an integer");
231 assert!(err.contains("max_tokens"), "{err}");
232 assert!(err.contains("integer"), "{err}");
233 assert!(err.contains("eight"), "{err}");
234 }
235
236 #[test]
237 fn required_str_distinguishes_missing_from_wrong_type() {
238 let missing = required_str(&json!({}), "model_path").expect_err("absent");
239 assert!(missing.contains("Missing required argument"));
240 assert!(missing.contains("model_path"));
241
242 let wrong = required_str(&json!({ "model_path": 7 }), "model_path").expect_err("not a str");
243 assert!(wrong.contains("model_path"));
244 assert!(wrong.contains("string"));
245 }
246}
247
248#[must_use]
263pub fn json_type_name(value: &serde_json::Value) -> &'static str {
264 match value {
265 serde_json::Value::Null => "null",
266 serde_json::Value::Bool(_) => "boolean",
267 serde_json::Value::Number(_) => "number",
268 serde_json::Value::String(_) => "string",
269 serde_json::Value::Array(_) => "array",
270 serde_json::Value::Object(_) => "object",
271 }
272}
273
274pub fn require_str<'a>(args: &'a serde_json::Value, name: &str) -> Result<&'a str, ToolCallResult> {
282 match args.get(name) {
283 Some(serde_json::Value::String(s)) => Ok(s.as_str()),
284 Some(other) => Err(ToolCallResult::error(format!(
285 "Argument {name} must be a string, got {}",
286 json_type_name(other)
287 ))),
288 None => Err(ToolCallResult::error(format!(
289 "Missing required argument: {name}"
290 ))),
291 }
292}
293
294#[cfg(test)]
295#[allow(clippy::disallowed_methods)] mod require_str_tests {
298 use super::*;
299
300 #[test]
301 fn present_string_is_returned() {
302 let args = serde_json::json!({ "model_path": "/tmp/m.gguf" });
303 assert_eq!(require_str(&args, "model_path").ok(), Some("/tmp/m.gguf"));
304 }
305
306 #[test]
307 fn absent_argument_says_missing() {
308 let args = serde_json::json!({});
309 let err = require_str(&args, "model_path").expect_err("absent must fail");
310 assert_eq!(err.is_error, Some(true));
311 assert_eq!(err.content[0].text, "Missing required argument: model_path");
312 }
313
314 #[test]
317 fn wrong_type_names_the_type_and_never_says_missing() {
318 for (value, expected_type) in [
319 (serde_json::json!(123), "number"),
320 (serde_json::json!(true), "boolean"),
321 (serde_json::json!(["/tmp/m.gguf"]), "array"),
322 (serde_json::json!({ "path": "/tmp/m.gguf" }), "object"),
323 (serde_json::json!(null), "null"),
324 ] {
325 let args = serde_json::json!({ "model_path": value });
326 let err = require_str(&args, "model_path").expect_err("wrong type must fail");
327 let text = &err.content[0].text;
328 assert_eq!(
329 text,
330 &format!("Argument model_path must be a string, got {expected_type}"),
331 "wrong-type message for {value}"
332 );
333 assert!(
334 !text.contains("Missing"),
335 "a supplied argument must never be reported as missing, got: {text}"
336 );
337 }
338 }
339
340 #[test]
341 fn non_object_arguments_read_as_missing() {
342 let args = serde_json::json!("notanobject");
343 let err = require_str(&args, "model_path").expect_err("non-object must fail");
344 assert_eq!(err.content[0].text, "Missing required argument: model_path");
345 }
346
347 #[test]
348 fn json_type_name_covers_every_variant() {
349 assert_eq!(json_type_name(&serde_json::json!(null)), "null");
350 assert_eq!(json_type_name(&serde_json::json!(false)), "boolean");
351 assert_eq!(json_type_name(&serde_json::json!(1.5)), "number");
352 assert_eq!(json_type_name(&serde_json::json!("s")), "string");
353 assert_eq!(json_type_name(&serde_json::json!([])), "array");
354 assert_eq!(json_type_name(&serde_json::json!({})), "object");
355 }
356}