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, 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(symbol: &str) -> Value {
196 json!({
197 "status": "NOT_FOUND",
198 "reason": "the selected symbol is not present in the active graph, or the name matches \
199 more than one symbol; pass an exact id",
200 "symbol": symbol,
201 })
202}
203
204pub(crate) fn stale_graph(file: &str) -> Value {
206 json!({
207 "status": "STALE_GRAPH",
208 "reason": format!(
209 "{file}: the recorded source range no longer matches the file. Rebuild the graph; \
210 nothing was planned from a range that cannot be located."
211 ),
212 })
213}
214
215pub(crate) fn invalid_args(operation: &str, missing: &[&str]) -> Value {
220 json!({
221 "status": "INVALID_ARGS",
222 "operation": operation,
223 "invalid": missing,
224 "reason": format!(
225 "missing or invalid required argument(s): {}. Nothing was planned or written.",
226 missing.join(", ")
227 ),
228 })
229}
230
231#[cfg(test)]
232mod tests {
233 use super::{Operation, call, catalog, catalog_names};
234 use crate::contract;
235
236 #[test]
237 fn every_contract_tool_resolves_to_an_operation() {
238 for tool in contract::tools() {
239 assert!(
240 Operation::from_name(&tool.name).is_some(),
241 "{} is in the contract with no operation arm",
242 tool.name
243 );
244 }
245 }
246
247 #[test]
248 fn no_operation_exists_outside_the_contract() {
249 for name in catalog_names() {
250 assert!(contract::declares(&name));
251 }
252 assert_eq!(catalog_names().len(), contract::tools().len());
253 }
254
255 #[test]
256 fn exactly_four_operations_can_write() {
257 let writers = contract::tools()
258 .iter()
259 .filter_map(|tool| Operation::from_name(&tool.name))
260 .filter(|operation| operation.writes())
261 .count();
262 assert_eq!(writers, 4);
263 }
264
265 #[test]
266 fn every_operation_answers_with_a_contract_status_ported_or_not() {
267 let state = crate::test_support::fixture_state();
268 for tool in contract::tools() {
269 let answer =
270 call(&state, &tool.name, &blazingly_json::json!({})).expect("declared tool");
271 let status = answer
272 .get("status")
273 .and_then(|value| value.as_str())
274 .unwrap_or_default();
275 assert!(
276 contract::permits_state(status),
277 "{} answered {status}, which is outside the contract",
278 tool.name
279 );
280 }
281 }
282
283 #[test]
284 fn an_undeclared_tool_is_an_error_not_a_status() {
285 let state = crate::test_support::fixture_state();
286 assert!(call(&state, "reformat_universe", &blazingly_json::json!({})).is_err());
287 }
288
289 #[test]
290 fn the_catalog_matches_the_contract() {
291 assert_eq!(
292 catalog().as_array().map(Vec::len),
293 Some(contract::tools().len())
294 );
295 }
296}