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