1use serde::{Deserialize, Serialize};
14
15use crate::{DocumentIr, ElementId, ElementIr, ErrorCode, FileMakerError, Length, Result, Style};
16
17#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
19pub struct Patch {
20 pub sequence: u64,
22 pub operations: Vec<PatchOperation>,
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28#[serde(tag = "op", rename_all = "snake_case")]
29pub enum PatchOperation {
30 SetText {
32 id: ElementId,
34 text: String,
36 },
37 SetHidden {
39 id: ElementId,
41 hidden: bool,
43 },
44 SetStyle {
46 id: ElementId,
48 style: Style,
50 },
51 Move {
53 id: ElementId,
55 x: Length,
57 y: Length,
59 },
60 Resize {
62 id: ElementId,
64 width: Length,
66 height: Length,
68 },
69 Remove {
71 id: ElementId,
73 },
74 Add {
76 parent: Option<ElementId>,
78 element: ElementIr,
80 },
81 Clone {
83 id: ElementId,
85 new_id: ElementId,
87 },
88 Replace {
90 id: ElementId,
92 element: ElementIr,
94 },
95}
96
97pub struct PatchTransaction<'a> {
99 document: &'a mut DocumentIr,
100 max_operations: usize,
101}
102
103impl<'a> PatchTransaction<'a> {
104 #[must_use]
106 pub fn new(document: &'a mut DocumentIr, max_operations: usize) -> Self {
107 Self {
108 document,
109 max_operations,
110 }
111 }
112
113 pub fn apply(&mut self, patch: &Patch) -> Result<()> {
115 self.apply_with_rollback_snapshot(patch).map(drop)
116 }
117
118 pub fn apply_with_rollback_snapshot(&mut self, patch: &Patch) -> Result<DocumentIr> {
120 if patch.operations.len() > self.max_operations {
121 return Err(FileMakerError::new(
122 ErrorCode::LimitExceeded,
123 "patch operation limit exceeded",
124 ));
125 }
126 let original = self.document.clone();
127 for operation in &patch.operations {
128 if let Err(error) = apply_operation(self.document, operation, patch.sequence) {
129 *self.document = original;
130 return Err(error);
131 }
132 }
133 if let Err(error) = Self::validate(self.document) {
134 *self.document = original;
135 return Err(error);
136 }
137 Ok(original)
138 }
139
140 pub fn validate(document: &DocumentIr) -> Result<()> {
142 validate_document_ids(&document.elements)?;
143 crate::layout_page::validate_page_layer_ir(document)
144 }
145}
146
147fn apply_operation(
148 document: &mut DocumentIr,
149 operation: &PatchOperation,
150 sequence: u64,
151) -> Result<()> {
152 match operation {
153 PatchOperation::SetText { id, text } => mutate(document, id, sequence, |element| {
154 element.text = Some(text.clone());
155 }),
156 PatchOperation::SetHidden { id, hidden } => {
157 mutate(document, id, sequence, |element| element.hidden = *hidden)
158 }
159 PatchOperation::SetStyle { id, style } => {
160 style.validate()?;
161 mutate(document, id, sequence, |element| {
162 element.style.overlay(style)
163 })
164 }
165 PatchOperation::Move { id, x, y } => mutate(document, id, sequence, |element| {
166 element.geometry.x = Some(*x);
167 element.geometry.y = Some(*y);
168 element.geometry.align_x = None;
169 element.geometry.align_y = None;
170 element.geometry.anchors.clear();
171 }),
172 PatchOperation::Resize { id, width, height } => mutate(document, id, sequence, |element| {
173 element.geometry.width = Some(*width);
174 element.geometry.height = Some(*height);
175 element.geometry.constraints = crate::LayoutConstraints::default();
176 }),
177 PatchOperation::Remove { id } => remove(&mut document.elements, id),
178 PatchOperation::Add { parent, element } => {
179 add(document, parent.as_ref(), element.clone(), sequence)
180 }
181 PatchOperation::Clone { id, new_id } => clone_element(document, id, new_id, sequence),
182 PatchOperation::Replace { id, element } => {
183 replace(&mut document.elements, id, element.clone(), sequence)
184 }
185 }
186}
187
188fn mutate(
189 document: &mut DocumentIr,
190 id: &ElementId,
191 sequence: u64,
192 action: impl FnOnce(&mut ElementIr),
193) -> Result<()> {
194 let element = find_mut(&mut document.elements, id)
195 .ok_or_else(|| patch_error("patch target was not found"))?;
196 ensure_unlocked(element)?;
197 action(element);
198 element.provenance.patches.push(sequence);
199 Ok(())
200}
201
202fn add(
203 document: &mut DocumentIr,
204 parent: Option<&ElementId>,
205 mut element: ElementIr,
206 sequence: u64,
207) -> Result<()> {
208 if find(&document.elements, &element.id).is_some() {
209 return Err(patch_error("added element ID already exists"));
210 }
211 element.provenance.patches.push(sequence);
212 if let Some(parent) = parent {
213 let parent = find_mut(&mut document.elements, parent)
214 .ok_or_else(|| patch_error("add parent was not found"))?;
215 ensure_unlocked(parent)?;
216 parent.children.push(element);
217 } else {
218 document.elements.push(element);
219 }
220 Ok(())
221}
222
223fn clone_element(
224 document: &mut DocumentIr,
225 id: &ElementId,
226 new_id: &ElementId,
227 sequence: u64,
228) -> Result<()> {
229 if find(&document.elements, new_id).is_some() {
230 return Err(patch_error("clone ID already exists"));
231 }
232 let mut cloned = find(&document.elements, id)
233 .ok_or_else(|| patch_error("clone source was not found"))?
234 .clone();
235 ensure_unlocked(&cloned)?;
236 remap_clone_ids(&mut cloned, id.as_str(), new_id.as_str())?;
237 cloned.provenance.patches.push(sequence);
238 document.elements.push(cloned);
239 Ok(())
240}
241
242fn remove(elements: &mut Vec<ElementIr>, id: &ElementId) -> Result<()> {
243 let target = find(elements, id).ok_or_else(|| patch_error("remove target was not found"))?;
244 ensure_subtree_unlocked(target)?;
245 remove_unchecked(elements, id);
246 Ok(())
247}
248
249fn remove_unchecked(elements: &mut Vec<ElementIr>, id: &ElementId) -> bool {
250 if let Some(position) = elements.iter().position(|element| &element.id == id) {
251 elements.remove(position);
252 return true;
253 }
254 for element in elements {
255 if remove_unchecked(&mut element.children, id) {
256 return true;
257 }
258 }
259 false
260}
261
262fn replace(
263 elements: &mut [ElementIr],
264 id: &ElementId,
265 mut replacement: ElementIr,
266 sequence: u64,
267) -> Result<()> {
268 let target = find(elements, id).ok_or_else(|| patch_error("replace target was not found"))?;
269 ensure_subtree_unlocked(target)?;
270 if target.page_placement != replacement.page_placement {
271 return Err(patch_error("replace cannot change page-layer ownership"));
272 }
273 if replace_unchecked(elements, id, &mut replacement, sequence) {
274 Ok(())
275 } else {
276 Err(patch_error("replace target was not found"))
277 }
278}
279
280fn replace_unchecked(
281 elements: &mut [ElementIr],
282 id: &ElementId,
283 replacement: &mut ElementIr,
284 sequence: u64,
285) -> bool {
286 for element in elements {
287 if &element.id == id {
288 replacement.provenance.patches.push(sequence);
289 *element = replacement.clone();
290 return true;
291 }
292 if replace_unchecked(&mut element.children, id, replacement, sequence) {
293 return true;
294 }
295 }
296 false
297}
298
299fn find<'a>(elements: &'a [ElementIr], id: &ElementId) -> Option<&'a ElementIr> {
300 for element in elements {
301 if &element.id == id {
302 return Some(element);
303 }
304 if let Some(found) = find(&element.children, id) {
305 return Some(found);
306 }
307 }
308 None
309}
310
311fn find_mut<'a>(elements: &'a mut [ElementIr], id: &ElementId) -> Option<&'a mut ElementIr> {
312 for element in elements {
313 if &element.id == id {
314 return Some(element);
315 }
316 if let Some(found) = find_mut(&mut element.children, id) {
317 return Some(found);
318 }
319 }
320 None
321}
322
323fn ensure_unlocked(element: &ElementIr) -> Result<()> {
324 if element.locked {
325 Err(
326 FileMakerError::new(ErrorCode::PatchLocked, "element is locked")
327 .at(element.id.as_str()),
328 )
329 } else {
330 Ok(())
331 }
332}
333
334fn ensure_subtree_unlocked(root: &ElementIr) -> Result<()> {
335 let mut stack = vec![root];
336 while let Some(element) = stack.pop() {
337 ensure_unlocked(element)?;
338 stack.extend(element.children.iter().rev());
339 }
340 Ok(())
341}
342
343fn remap_clone_ids(element: &mut ElementIr, old_root: &str, new_root: &str) -> Result<()> {
344 let current = element.id.as_str();
345 let suffix = current.strip_prefix(old_root).unwrap_or(current);
346 element.id = ElementId::new(format!("{new_root}{suffix}"))?;
347 for child in &mut element.children {
348 remap_clone_ids(child, old_root, new_root)?;
349 }
350 Ok(())
351}
352
353fn validate_document_ids(elements: &[ElementIr]) -> Result<()> {
354 let mut ids = std::collections::BTreeSet::new();
355 let mut stack: Vec<&ElementIr> = elements.iter().collect();
356 while let Some(element) = stack.pop() {
357 if !ids.insert(element.id.as_str()) {
358 return Err(patch_error(format!(
359 "patch produced duplicate element ID `{}`",
360 element.id.as_str()
361 )));
362 }
363 stack.extend(&element.children);
364 }
365 Ok(())
366}
367
368fn patch_error(message: impl Into<String>) -> FileMakerError {
369 FileMakerError::new(ErrorCode::PatchInvalid, message)
370}
371
372#[cfg(test)]
373mod operation_log_tests {
374 use std::collections::BTreeMap;
375
376 use super::*;
377 use crate::{Compiler, DataValue, OperationLog};
378
379 fn document() -> DocumentIr {
380 let yaml = br"filemaker: '1.0'
381model: canvas
382id: log
383page: { width: 10pt, height: 10pt }
384elements: [{ id: node, type: rect, width: 2pt, height: 2pt }]
385";
386 let compiler = Compiler::builder().build().unwrap();
387 let template = compiler.compile_template_yaml(yaml).unwrap();
388 compiler
389 .bind(&template, &DataValue::Object(BTreeMap::new()), &[])
390 .unwrap()
391 }
392
393 fn visibility_patch(sequence: u64, hidden: bool) -> Patch {
394 Patch {
395 sequence,
396 operations: vec![PatchOperation::SetHidden {
397 id: ElementId::new("node").unwrap(),
398 hidden,
399 }],
400 }
401 }
402
403 #[test]
404 fn successful_patches_can_be_undone_and_redone() {
405 let mut document = document();
406 let patch = visibility_patch(1, true);
407 let mut log = OperationLog::new(2).unwrap();
408 log.apply(&mut document, &patch, 4).unwrap();
409 assert!(document.elements[0].hidden);
410 log.undo(&mut document).unwrap();
411 assert!(!document.elements[0].hidden);
412 log.redo(&mut document).unwrap();
413 assert!(document.elements[0].hidden);
414 }
415
416 #[test]
417 fn rejects_a_snapshot_before_mutating_the_document() {
418 let mut document = document();
419 let original = document.clone();
420 let mut log = OperationLog::new_bounded(2, 1).unwrap();
421 let error = log
422 .apply(&mut document, &visibility_patch(1, true), 4)
423 .unwrap_err();
424 assert_eq!(error.code(), ErrorCode::LimitExceeded);
425 assert_eq!(document, original);
426 assert_eq!(log.used_bytes(), 0);
427 }
428
429 #[test]
430 fn evicts_old_snapshots_to_honor_the_aggregate_byte_budget() {
431 let mut document = document();
432 let original_bytes = crate::memory::serialized_size(&document).unwrap();
433 let mut changed = document.clone();
434 PatchTransaction::new(&mut changed, 1)
435 .apply(&visibility_patch(1, true))
436 .unwrap();
437 let changed_bytes = crate::memory::serialized_size(&changed).unwrap();
438 let mut log = OperationLog::new_bounded(4, original_bytes.max(changed_bytes)).unwrap();
439 log.apply(&mut document, &visibility_patch(1, true), 1)
440 .unwrap();
441 log.apply(&mut document, &visibility_patch(2, false), 1)
442 .unwrap();
443 assert_eq!(log.undo_len(), 1);
444 assert!(log.used_bytes() <= log.max_bytes());
445 }
446}