1use crate::core::events::{DryRunImpact, OpEvent, ShiftedSpan};
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub enum EngineStructureOp {
28 InsertRows {
29 sheet_name: String,
30 at_row: u32,
31 count: u32,
32 },
33 DeleteRows {
34 sheet_name: String,
35 start_row: u32,
36 count: u32,
37 },
38 InsertCols {
39 sheet_name: String,
40 at_col: u32,
41 count: u32,
42 },
43 DeleteCols {
44 sheet_name: String,
45 start_col: u32,
46 count: u32,
47 },
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct EngineApplyResult {
53 pub formulas_rewritten: u64,
54 pub ref_errors_generated: u64,
55 pub shifted_spans: Vec<ShiftedSpan>,
56}
57
58pub fn op_event_to_engine_op(event: &OpEvent) -> Option<EngineStructureOp> {
62 let payload = &event.payload;
63 let kind = &event.kind.0;
64
65 match kind.as_str() {
66 "structure.insert_rows" => Some(EngineStructureOp::InsertRows {
67 sheet_name: payload.get("sheet_name")?.as_str()?.to_string(),
68 at_row: payload.get("at_row")?.as_u64()? as u32,
69 count: payload.get("count")?.as_u64().unwrap_or(1) as u32,
70 }),
71 "structure.delete_rows" => Some(EngineStructureOp::DeleteRows {
72 sheet_name: payload.get("sheet_name")?.as_str()?.to_string(),
73 start_row: payload.get("start_row")?.as_u64()? as u32,
74 count: payload.get("count")?.as_u64().unwrap_or(1) as u32,
75 }),
76 "structure.insert_cols" => Some(EngineStructureOp::InsertCols {
77 sheet_name: payload.get("sheet_name")?.as_str()?.to_string(),
78 at_col: payload.get("at_col")?.as_u64()? as u32,
79 count: payload.get("count")?.as_u64().unwrap_or(1) as u32,
80 }),
81 "structure.delete_cols" => Some(EngineStructureOp::DeleteCols {
82 sheet_name: payload.get("sheet_name")?.as_str()?.to_string(),
83 start_col: payload.get("start_col")?.as_u64()? as u32,
84 count: payload.get("count")?.as_u64().unwrap_or(1) as u32,
85 }),
86 _ => None,
87 }
88}
89
90pub fn predict_engine_impact(op: &EngineStructureOp) -> DryRunImpact {
96 let shifted_span = match op {
97 EngineStructureOp::InsertRows {
98 sheet_name,
99 at_row,
100 count,
101 } => ShiftedSpan {
102 op_index: 0,
103 sheet_name: sheet_name.clone(),
104 axis: "row".to_string(),
105 at: *at_row,
106 count: *count,
107 direction: "insert".to_string(),
108 description: format!("rows {}..inf shift +{}", at_row, count),
109 },
110 EngineStructureOp::DeleteRows {
111 sheet_name,
112 start_row,
113 count,
114 } => ShiftedSpan {
115 op_index: 0,
116 sheet_name: sheet_name.clone(),
117 axis: "row".to_string(),
118 at: *start_row,
119 count: *count,
120 direction: "delete".to_string(),
121 description: format!("rows {}..inf shift -{}", start_row, count),
122 },
123 EngineStructureOp::InsertCols {
124 sheet_name,
125 at_col,
126 count,
127 } => ShiftedSpan {
128 op_index: 0,
129 sheet_name: sheet_name.clone(),
130 axis: "col".to_string(),
131 at: *at_col,
132 count: *count,
133 direction: "insert".to_string(),
134 description: format!("cols {}..inf shift +{}", at_col, count),
135 },
136 EngineStructureOp::DeleteCols {
137 sheet_name,
138 start_col,
139 count,
140 } => ShiftedSpan {
141 op_index: 0,
142 sheet_name: sheet_name.clone(),
143 axis: "col".to_string(),
144 at: *start_col,
145 count: *count,
146 direction: "delete".to_string(),
147 description: format!("cols {}..inf shift -{}", start_col, count),
148 },
149 };
150
151 DryRunImpact {
152 cells_changed: 0, formulas_rewritten: 0,
154 shifted_spans: vec![shifted_span],
155 ref_errors_generated: 0,
156 warnings: Vec::new(),
157 boundary_warnings: Vec::new(),
158 }
159}
160
161pub fn change_event_to_op_payload(
174 _engine_event_kind: &str,
175 _engine_event_data: &serde_json::Value,
176) -> serde_json::Value {
177 serde_json::json!({
180 "engine_native": true,
181 "mapping": "placeholder"
182 })
183}
184
185#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::core::events::{Actor, OpEvent, OpKind};
193 use serde_json::json;
194
195 #[test]
196 fn convert_insert_rows_event() {
197 let event = OpEvent::new(
198 "sess_test".to_string(),
199 None,
200 Actor {
201 id: "test".to_string(),
202 run_id: None,
203 source: "test".to_string(),
204 },
205 OpKind::structure_insert_rows(),
206 json!({
207 "sheet_name": "Sheet1",
208 "at_row": 5,
209 "count": 3
210 }),
211 );
212
213 let engine_op = op_event_to_engine_op(&event).unwrap();
214 match engine_op {
215 EngineStructureOp::InsertRows {
216 sheet_name,
217 at_row,
218 count,
219 } => {
220 assert_eq!(sheet_name, "Sheet1");
221 assert_eq!(at_row, 5);
222 assert_eq!(count, 3);
223 }
224 _ => panic!("expected InsertRows"),
225 }
226 }
227
228 #[test]
229 fn predict_insert_rows_impact() {
230 let op = EngineStructureOp::InsertRows {
231 sheet_name: "Sheet1".to_string(),
232 at_row: 10,
233 count: 2,
234 };
235 let impact = predict_engine_impact(&op);
236 assert_eq!(impact.shifted_spans.len(), 1);
237 assert_eq!(impact.shifted_spans[0].axis, "row");
238 assert_eq!(impact.shifted_spans[0].at, 10);
239 assert_eq!(impact.shifted_spans[0].count, 2);
240 assert_eq!(impact.shifted_spans[0].direction, "insert");
241 }
242
243 #[test]
244 fn non_structural_event_returns_none() {
245 let event = OpEvent::new(
246 "sess_test".to_string(),
247 None,
248 Actor {
249 id: "test".to_string(),
250 run_id: None,
251 source: "test".to_string(),
252 },
253 OpKind::edit_batch(),
254 json!({"cell": "A1"}),
255 );
256
257 assert!(op_event_to_engine_op(&event).is_none());
258 }
259}