weavatrix_rust_refactor/operations/
mod.rs1mod apply;
12mod bulk_replace;
13mod change_signature;
14mod delete_readiness;
15mod edit_symbol;
16mod move_file;
17mod move_symbol;
18mod organize_imports;
19mod rename_related;
20mod rename_symbol;
21mod signature;
22
23use crate::contract;
24use crate::token::TokenStore;
25use blazingly_json::{Value, json};
26use weavatrix_rust::RepositoryState;
27
28pub struct RefactorSession {
34 tokens: TokenStore,
35 write_allowed: bool,
36}
37
38impl RefactorSession {
39 #[must_use]
41 pub fn new(write_allowed: bool) -> Self {
42 Self {
43 tokens: TokenStore::default(),
44 write_allowed,
45 }
46 }
47
48 #[must_use]
53 pub fn read_only() -> Self {
54 Self::new(false)
55 }
56
57 pub fn call(
65 &self,
66 state: &RepositoryState,
67 name: &str,
68 arguments: &Value,
69 ) -> Result<Value, String> {
70 let Some(operation) = Operation::from_name(name) else {
71 return Err(format!("unknown refactor operation: {name}"));
72 };
73 Ok(match operation {
74 Operation::DeleteReadiness => delete_readiness::delete_readiness(state, arguments),
75 Operation::EditSymbol => edit_symbol::edit_symbol(state, arguments),
76 Operation::BulkReplace => bulk_replace::bulk_replace(state, arguments),
77 Operation::MoveSymbol => move_symbol::move_symbol(state, arguments),
78 Operation::MoveFile => move_file::move_file(state, arguments),
79 Operation::OrganizeImports => organize_imports::organize_imports(state, arguments),
80 Operation::RenameSymbol => rename_symbol::rename_symbol(state, &self.tokens, arguments),
81 Operation::RenameRelatedSymbols => rename_related::rename_related_symbols(
82 state,
83 &self.tokens,
84 arguments,
85 self.write_allowed,
86 ),
87 Operation::ApplyEditPlan => {
88 apply::apply_edit_plan(state.root(), &self.tokens, arguments, self.write_allowed)
89 }
90 Operation::RollbackLastApply => {
91 apply::rollback_last_apply(state.root(), self.write_allowed)
92 }
93 Operation::ChangeSignature => change_signature::change_signature(state, arguments),
94 })
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum Operation {
101 RenameSymbol,
102 RenameRelatedSymbols,
103 ApplyEditPlan,
104 RollbackLastApply,
105 ChangeSignature,
106 EditSymbol,
107 BulkReplace,
108 OrganizeImports,
109 MoveFile,
110 MoveSymbol,
111 DeleteReadiness,
112}
113
114impl Operation {
115 #[must_use]
117 pub fn from_name(name: &str) -> Option<Self> {
118 match name {
119 "rename_symbol" => Some(Self::RenameSymbol),
120 "rename_related_symbols" => Some(Self::RenameRelatedSymbols),
121 "apply_edit_plan" => Some(Self::ApplyEditPlan),
122 "rollback_last_apply" => Some(Self::RollbackLastApply),
123 "change_signature" => Some(Self::ChangeSignature),
124 "edit_symbol" => Some(Self::EditSymbol),
125 "bulk_replace" => Some(Self::BulkReplace),
126 "organize_imports" => Some(Self::OrganizeImports),
127 "move_file" => Some(Self::MoveFile),
128 "move_symbol" => Some(Self::MoveSymbol),
129 "delete_readiness" => Some(Self::DeleteReadiness),
130 _ => None,
131 }
132 }
133
134 #[must_use]
139 pub const fn writes(self) -> bool {
140 matches!(
141 self,
142 Self::RenameSymbol
143 | Self::RenameRelatedSymbols
144 | Self::ApplyEditPlan
145 | Self::RollbackLastApply
146 )
147 }
148
149 #[must_use]
151 pub const fn name(self) -> &'static str {
152 match self {
153 Self::RenameSymbol => "rename_symbol",
154 Self::RenameRelatedSymbols => "rename_related_symbols",
155 Self::ApplyEditPlan => "apply_edit_plan",
156 Self::RollbackLastApply => "rollback_last_apply",
157 Self::ChangeSignature => "change_signature",
158 Self::EditSymbol => "edit_symbol",
159 Self::BulkReplace => "bulk_replace",
160 Self::OrganizeImports => "organize_imports",
161 Self::MoveFile => "move_file",
162 Self::MoveSymbol => "move_symbol",
163 Self::DeleteReadiness => "delete_readiness",
164 }
165 }
166}
167
168#[must_use]
170pub fn catalog() -> Value {
171 contract::catalog_value().clone()
172}
173
174#[must_use]
176pub fn catalog_names() -> Vec<String> {
177 contract::tools()
178 .iter()
179 .map(|tool| tool.name.clone())
180 .collect()
181}
182
183pub fn call(state: &RepositoryState, name: &str, arguments: &Value) -> Result<Value, String> {
191 RefactorSession::read_only().call(state, name, arguments)
192}
193
194pub(crate) fn not_found(graph: &weavatrix_graph::Graph, symbol: &str) -> Value {
199 let candidates = crate::resolve::candidate_ids(graph, symbol);
200 json!({
201 "status": "NOT_FOUND",
202 "reason": if candidates.len() > 1 {
203 "the name matches more than one symbol; pass one of the candidate ids"
204 } else {
205 "the selected symbol is not present in the active graph; pass an exact id"
206 },
207 "symbol": symbol,
208 "candidates": candidates,
209 })
210}
211
212pub(crate) fn stale_graph(file: &str) -> Value {
214 json!({
215 "status": "STALE_GRAPH",
216 "reason": format!(
217 "{file}: the recorded source range no longer matches the file. Rebuild the graph; \
218 nothing was planned from a range that cannot be located."
219 ),
220 })
221}
222
223pub(crate) fn invalid_args(operation: &str, missing: &[&str]) -> Value {
228 json!({
229 "status": "INVALID_ARGS",
230 "operation": operation,
231 "invalid": missing,
232 "reason": format!(
233 "missing or invalid required argument(s): {}. Nothing was planned or written.",
234 missing.join(", ")
235 ),
236 })
237}
238
239#[cfg(test)]
240mod tests {
241 use super::{Operation, call, catalog, catalog_names};
242 use crate::contract;
243
244 #[test]
245 fn every_contract_tool_resolves_to_an_operation() {
246 for tool in contract::tools() {
247 assert!(
248 Operation::from_name(&tool.name).is_some(),
249 "{} is in the contract with no operation arm",
250 tool.name
251 );
252 }
253 }
254
255 #[test]
256 fn no_operation_exists_outside_the_contract() {
257 for name in catalog_names() {
258 assert!(contract::declares(&name));
259 }
260 assert_eq!(catalog_names().len(), contract::tools().len());
261 }
262
263 #[test]
264 fn exactly_four_operations_can_write() {
265 let writers = contract::tools()
266 .iter()
267 .filter_map(|tool| Operation::from_name(&tool.name))
268 .filter(|operation| operation.writes())
269 .count();
270 assert_eq!(writers, 4);
271 }
272
273 #[test]
274 fn every_operation_answers_with_a_contract_status_ported_or_not() {
275 let state = crate::test_support::fixture_state();
276 for tool in contract::tools() {
277 let answer =
278 call(&state, &tool.name, &blazingly_json::json!({})).expect("declared tool");
279 let status = answer
280 .get("status")
281 .and_then(|value| value.as_str())
282 .unwrap_or_default();
283 assert!(
284 contract::permits_state(status),
285 "{} answered {status}, which is outside the contract",
286 tool.name
287 );
288 }
289 }
290
291 #[test]
292 fn an_undeclared_tool_is_an_error_not_a_status() {
293 let state = crate::test_support::fixture_state();
294 assert!(call(&state, "reformat_universe", &blazingly_json::json!({})).is_err());
295 }
296
297 #[test]
298 fn the_catalog_matches_the_contract() {
299 assert_eq!(
300 catalog().as_array().map(Vec::len),
301 Some(contract::tools().len())
302 );
303 }
304}