Skip to main content

artifactize_tools/builtin/
input.rs

1//! Parse schema-validated provider arguments once, before builtin execution.
2use crate::Builtin;
3use serde::{Deserialize, Deserializer, de};
4use serde_json::Value;
5
6#[derive(Debug)]
7pub enum Input {
8    Read(Read),
9    List(List),
10    Glob(Glob),
11    Grep(Grep),
12    ViewImage(ViewImage),
13}
14impl Input {
15    pub fn parse(builtin: Builtin, value: Value) -> Result<Self, String> {
16        let validator = crate::schema::compile(&super::input_schema(builtin))?;
17        crate::schema::validate(&validator, &value)?;
18        let input = match builtin {
19            Builtin::Graph | Builtin::Section | Builtin::Help | Builtin::Open => {
20                return Err("This builtin requires declared args.".into());
21            }
22            Builtin::Read => serde_json::from_value(value).map(Self::Read),
23            Builtin::List => serde_json::from_value(value).map(Self::List),
24            Builtin::Glob => serde_json::from_value(value).map(Self::Glob),
25            Builtin::Grep => serde_json::from_value(value).map(Self::Grep),
26            Builtin::ViewImage => serde_json::from_value(value).map(Self::ViewImage),
27        };
28        input.map_err(|_| "Tool arguments cannot be read as the declared builtin input.".into())
29    }
30}
31
32#[derive(Debug, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct Read {
35    pub path: String,
36    #[serde(default = "first_line", deserialize_with = "integer")]
37    pub offset: usize,
38    #[serde(default = "read_lines", deserialize_with = "integer")]
39    pub limit: usize,
40}
41#[derive(Debug, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct List {
44    #[serde(default)]
45    pub path: String,
46    #[serde(default, deserialize_with = "integer")]
47    pub offset: usize,
48    #[serde(default = "max_results", deserialize_with = "integer")]
49    pub limit: usize,
50}
51#[derive(Debug, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct Glob {
54    pub pattern: String,
55    #[serde(default)]
56    pub path: String,
57}
58#[derive(Debug, Deserialize)]
59#[serde(rename_all = "camelCase", deny_unknown_fields)]
60pub struct Grep {
61    pub pattern: String,
62    #[serde(default)]
63    pub path: String,
64    #[serde(default)]
65    pub glob: Option<String>,
66    #[serde(default)]
67    pub case_insensitive: bool,
68    #[serde(default = "max_results", deserialize_with = "integer")]
69    pub max_results: usize,
70}
71#[derive(Debug, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct ViewImage {
74    pub path: String,
75}
76
77fn first_line() -> usize {
78    1
79}
80fn read_lines() -> usize {
81    super::DEFAULT_READ_LINES
82}
83fn max_results() -> usize {
84    super::MAX_RESULTS
85}
86
87/// JSON Schema treats 1 and 1.0 as the same integer. Preserve the validator's numeric
88/// semantics and the previous saturating usize conversion, including 32-bit hosts.
89fn integer<'de, D: Deserializer<'de>>(deserializer: D) -> Result<usize, D::Error> {
90    struct Integer;
91    impl de::Visitor<'_> for Integer {
92        type Value = usize;
93        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94            formatter.write_str("a nonnegative safe JSON integer")
95        }
96        fn visit_u64<E: de::Error>(self, value: u64) -> Result<usize, E> {
97            self.visit_f64(value as f64)
98        }
99        fn visit_i64<E: de::Error>(self, value: i64) -> Result<usize, E> {
100            self.visit_f64(value as f64)
101        }
102        fn visit_f64<E: de::Error>(self, value: f64) -> Result<usize, E> {
103            if value.is_finite()
104                && value >= 0.0
105                && value.fract() == 0.0
106                && value <= crate::MAX_SAFE_JSON_INTEGER as f64
107            {
108                Ok(value as usize)
109            } else {
110                Err(E::custom("expected a nonnegative safe JSON integer"))
111            }
112        }
113    }
114    deserializer.deserialize_any(Integer)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use serde_json::json;
121
122    #[test]
123    fn edge_rejects_out_of_range_and_unknown_arguments_without_caller_validation() {
124        for (builtin, value) in [
125            (Builtin::Read, json!({"path":"notes", "offset":0})),
126            (Builtin::Read, json!({"path":"notes", "limit":501})),
127            (Builtin::List, json!({"limit":201})),
128            (Builtin::Grep, json!({"pattern":"notes", "maxResults":0})),
129            (Builtin::Glob, json!({"pattern":""})),
130            (Builtin::ViewImage, json!({"path":"image", "unknown":true})),
131        ] {
132            assert!(Input::parse(builtin, value).is_err());
133        }
134        let parsed = Input::parse(
135            Builtin::Read,
136            json!({"path":"notes", "offset":1.0, "limit":2.0}),
137        )
138        .unwrap();
139        let Input::Read(input) = parsed else {
140            panic!("expected a read input")
141        };
142        assert_eq!((input.offset, input.limit), (1, 2));
143    }
144}