specfold_core/authoring/
plan.rs1use anyhow::{Context, Result};
5use serde::Serialize;
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize)]
9#[serde(tag = "op", rename_all = "lowercase")]
10pub enum Change {
11 Write {
12 path: String,
13 content: String,
14 created: bool,
15 },
16 Move {
17 from: String,
18 to: String,
19 },
20 Copy {
22 from: String,
23 to: String,
24 },
25 Remove {
26 path: String,
27 },
28}
29
30#[derive(Debug, Clone, Default, Serialize)]
31pub struct Plan {
32 pub changes: Vec<Change>,
33}
34
35impl Plan {
36 pub fn write(&mut self, path: impl Into<String>, content: String, created: bool) {
37 let path = path.into();
38 self.changes.retain(|c| !matches!(c, Change::Write { path: p, .. } if *p == path));
39 self.changes.push(Change::Write { path, content, created });
40 }
41
42 pub fn mv(&mut self, from: impl Into<String>, to: impl Into<String>) {
43 self.changes.push(Change::Move { from: from.into(), to: to.into() });
44 }
45
46 pub fn copy(&mut self, from: impl Into<String>, to: impl Into<String>) {
47 self.changes.push(Change::Copy { from: from.into(), to: to.into() });
48 }
49
50 pub fn remove(&mut self, path: impl Into<String>) {
51 self.changes.push(Change::Remove { path: path.into() });
52 }
53
54 pub fn is_empty(&self) -> bool {
55 self.changes.is_empty()
56 }
57
58 pub fn paths(&self) -> Vec<String> {
60 self.changes
61 .iter()
62 .map(|c| match c {
63 Change::Write { path, .. } | Change::Remove { path } => path.clone(),
64 Change::Move { from, to } | Change::Copy { from, to } => format!("{from} -> {to}"),
65 })
66 .collect()
67 }
68
69 pub fn apply(&self, root: &Path) -> Result<()> {
71 let ensure_parent = |p: &Path| -> Result<()> {
72 if let Some(d) = p.parent() {
73 std::fs::create_dir_all(d)?;
74 }
75 Ok(())
76 };
77 for c in &self.changes {
78 match c {
79 Change::Move { from, to } => {
80 let dst = root.join(to);
81 ensure_parent(&dst)?;
82 std::fs::rename(root.join(from), &dst).with_context(|| format!("cannot move {from} to {to}"))?;
83 }
84 Change::Copy { from, to } => {
85 let dst = root.join(to);
86 ensure_parent(&dst)?;
87 std::fs::copy(root.join(from), &dst).with_context(|| format!("cannot copy {from} to {to}"))?;
88 }
89 _ => {}
90 }
91 }
92 for c in &self.changes {
93 if let Change::Write { path, content, .. } = c {
94 let dst = root.join(path);
95 ensure_parent(&dst)?;
96 std::fs::write(&dst, content).with_context(|| format!("cannot write {path}"))?;
97 }
98 }
99 for c in &self.changes {
100 if let Change::Remove { path } = c {
101 let p = root.join(path);
102 if p.is_dir() {
103 std::fs::remove_dir_all(&p).with_context(|| format!("cannot remove {path}"))?;
104 } else if p.exists() {
105 std::fs::remove_file(&p).with_context(|| format!("cannot remove {path}"))?;
106 }
107 }
108 }
109 Ok(())
110 }
111}