1use serde_json::{json, Value};
2
3use crate::ToolError;
4
5use super::path::state_root;
6use super::path::VirtualPath;
7use super::workspace::{FileEdit, StateWorkspace};
8
9#[derive(Debug, Clone)]
10pub struct StateProvider {
11 workspace: StateWorkspace,
12}
13
14impl Default for StateProvider {
15 fn default() -> Self {
16 Self {
17 workspace: StateWorkspace::new(state_root()),
18 }
19 }
20}
21
22impl StateProvider {
23 pub fn new(workspace: StateWorkspace) -> Self {
24 Self { workspace }
25 }
26
27 pub async fn dispatch(&self, method: &str, params: Value) -> Result<Value, ToolError> {
28 match method {
29 "write_file" => {
30 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
31 let content = string_param(¶ms, "content")?;
32 self.workspace.write_file(&path, content).await?;
33 Ok(json!({"ok": true}))
34 }
35 "append_file" => {
36 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
37 Ok(json!(
38 self.workspace
39 .append_file(&path, string_param(¶ms, "content")?)
40 .await?
41 ))
42 }
43 "read_file" => {
44 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
45 Ok(json!(self.workspace.read_file(&path).await?))
46 }
47 "read_json" => {
48 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
49 Ok(json!(self.workspace.read_json(&path).await?))
50 }
51 "write_json" => {
52 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
53 self.workspace
54 .write_json(
55 &path,
56 value_param(¶ms, "value")?,
57 bool_param(¶ms, "pretty", false),
58 )
59 .await?;
60 Ok(json!({"ok": true}))
61 }
62 "hash_file" => {
63 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
64 Ok(json!(
65 self.workspace
66 .hash_file(&path, string_param_default(¶ms, "algorithm", "sha256"))
67 .await?
68 ))
69 }
70 "detect_file" => {
71 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
72 Ok(json!(self.workspace.detect_file(&path).await?))
73 }
74 "exists" => {
75 let path =
76 VirtualPath::parse_read_scope(string_param_default(¶ms, "path", ""))?;
77 Ok(json!(self.workspace.exists(&path).await?))
78 }
79 "stat" => {
80 let path =
81 VirtualPath::parse_read_scope(string_param_default(¶ms, "path", ""))?;
82 Ok(json!(self.workspace.stat(&path).await?))
83 }
84 "mkdir" => {
85 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
86 Ok(json!(self.workspace.mkdir(&path).await?))
87 }
88 "remove" => {
89 let path = VirtualPath::parse(string_param(¶ms, "path")?)?;
90 Ok(json!(
91 self.workspace
92 .remove(&path, bool_param(¶ms, "recursive", false))
93 .await?
94 ))
95 }
96 "copy" => {
97 let from = VirtualPath::parse(string_param(¶ms, "from")?)?;
98 let to = VirtualPath::parse(string_param(¶ms, "to")?)?;
99 Ok(json!(self.workspace.copy(&from, &to).await?))
100 }
101 "move" => {
102 let from = VirtualPath::parse(string_param(¶ms, "from")?)?;
103 let to = VirtualPath::parse(string_param(¶ms, "to")?)?;
104 Ok(json!(self.workspace.move_path(&from, &to).await?))
105 }
106 "walk_tree" => {
107 let path =
108 VirtualPath::parse_read_scope(string_param_default(¶ms, "path", ""))?;
109 Ok(json!(
110 self.workspace
111 .walk_tree(&path, usize_param(¶ms, "limit", 200))
112 .await?
113 ))
114 }
115 "list" => {
116 let path =
117 VirtualPath::parse_read_scope(string_param_default(¶ms, "path", ""))?;
118 Ok(json!(self.workspace.list(&path).await?))
119 }
120 "glob" => Ok(json!(
121 self.workspace
122 .glob(
123 string_param(¶ms, "pattern")?,
124 usize_param(¶ms, "limit", 200)
125 )
126 .await?
127 )),
128 "search_files" => Ok(json!(
129 self.workspace
130 .search_files(
131 string_param(¶ms, "pattern")?,
132 string_param(¶ms, "query")?,
133 usize_param(¶ms, "limit", 200)
134 )
135 .await?
136 )),
137 "replace_in_files" => Ok(json!(
138 self.workspace
139 .replace_in_files(
140 string_param(¶ms, "pattern")?,
141 string_param(¶ms, "search")?,
142 string_param(¶ms, "replace")?,
143 bool_param(¶ms, "dry_run", true)
144 )
145 .await?
146 )),
147 "plan_edits" => Ok(json!(
148 self.workspace
149 .plan_edits(edits_param(¶ms, "edits")?)
150 .await?
151 )),
152 "apply_edit_plan" => Ok(json!(
153 self.workspace
154 .apply_edit_plan(string_param(¶ms, "plan_id")?)
155 .await?
156 )),
157 "status" => Ok(json!({"root": self.workspace.root().display().to_string()})),
158 _ => Err(ToolError::UnknownAction {
159 message: format!("unknown state method `{method}`"),
160 valid: state_methods().iter().map(ToString::to_string).collect(),
161 hint: None,
162 }),
163 }
164 }
165}
166
167fn state_methods() -> &'static [&'static str] {
168 &[
169 "append_file",
170 "apply_edit_plan",
171 "copy",
172 "detect_file",
173 "exists",
174 "glob",
175 "hash_file",
176 "list",
177 "mkdir",
178 "move",
179 "plan_edits",
180 "read_file",
181 "read_json",
182 "remove",
183 "replace_in_files",
184 "search_files",
185 "stat",
186 "status",
187 "walk_tree",
188 "write_file",
189 "write_json",
190 ]
191}
192
193fn string_param<'a>(params: &'a Value, key: &str) -> Result<&'a str, ToolError> {
194 params
195 .get(key)
196 .and_then(Value::as_str)
197 .ok_or_else(|| ToolError::MissingParam {
198 message: format!("missing `{key}`"),
199 param: key.to_string(),
200 })
201}
202
203fn string_param_default<'a>(params: &'a Value, key: &str, default: &'a str) -> &'a str {
204 params.get(key).and_then(Value::as_str).unwrap_or(default)
205}
206
207fn bool_param(params: &Value, key: &str, default: bool) -> bool {
208 params.get(key).and_then(Value::as_bool).unwrap_or(default)
209}
210
211fn usize_param(params: &Value, key: &str, default: usize) -> usize {
212 params
213 .get(key)
214 .and_then(Value::as_u64)
215 .and_then(|value| usize::try_from(value).ok())
216 .unwrap_or(default)
217}
218
219fn value_param<'a>(params: &'a Value, key: &str) -> Result<&'a Value, ToolError> {
220 params.get(key).ok_or_else(|| ToolError::MissingParam {
221 message: format!("missing `{key}`"),
222 param: key.to_string(),
223 })
224}
225
226fn edits_param(params: &Value, key: &str) -> Result<Vec<FileEdit>, ToolError> {
227 let value = value_param(params, key)?;
228 serde_json::from_value(value.clone()).map_err(|err| ToolError::InvalidParam {
229 message: format!("invalid state edit list: {err}"),
230 param: key.to_string(),
231 })
232}