1use serde::{Deserialize, Serialize};
5use todoapp_core::{ComponentStore, Id, Link, LinkKind, Status, TaskEntityStore};
6
7use crate::service::{Error, Services, TaskSnapshot};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Export {
13 pub tasks: Vec<TaskSnapshot>,
14 pub links: Vec<Link>,
15}
16
17impl<'a, St: ComponentStore + TaskEntityStore> Services<'a, St> {
18 pub async fn export(&self, root: &Id) -> Result<Export, Error> {
20 let mut ids = self.descendants(root).await;
21 ids.insert(root.clone());
22
23 let mut tasks: Vec<TaskSnapshot> = Vec::new();
24 for id in &ids {
25 if let Ok(t) = self.snapshot(id).await {
26 tasks.push(t);
27 }
28 }
29 tasks.sort_by(|a, b| a.id.cmp(&b.id));
30
31 let mut links = Vec::new();
32 for id in &ids {
33 for kind in [LinkKind::Child, LinkKind::Blocks] {
34 for l in self.links.outgoing(id, kind).await {
35 if ids.contains(&l.to) {
36 links.push(l);
37 }
38 }
39 }
40 }
41 links.sort_by(|a, b| {
42 (a.from.as_str(), a.to.as_str(), format!("{:?}", a.kind)).cmp(&(
43 b.from.as_str(),
44 b.to.as_str(),
45 format!("{:?}", b.kind),
46 ))
47 });
48 Ok(Export { tasks, links })
49 }
50
51 pub async fn export_json(&self, root: &Id) -> Result<String, Error> {
52 let export = self.export(root).await?;
53 serde_json::to_string_pretty(&export).map_err(|e| Error::Import(e.to_string()))
54 }
55
56 pub async fn import_json(&self, json: &str) -> Result<(), Error> {
58 let export: Export =
59 serde_json::from_str(json).map_err(|e| Error::Import(e.to_string()))?;
60 for task in &export.tasks {
61 self.write_snapshot(task).await;
62 }
63 let parented: std::collections::HashSet<&Id> = export
66 .links
67 .iter()
68 .filter(|l| l.kind == LinkKind::Child)
69 .map(|l| &l.to)
70 .collect();
71 for link in &export.links {
72 self.links.put(link.clone()).await;
73 }
74 for task in &export.tasks {
77 if !parented.contains(&task.id) {
78 self.attach(&task.id, &Id::root(), None).await?;
79 }
80 }
81 Ok(())
82 }
83
84 pub async fn export_md(&self, root: &Id) -> Result<String, Error> {
87 let mut out = String::new();
88 let mut stack = vec![(root.clone(), 0usize)];
89 while let Some((id, depth)) = stack.pop() {
90 let task = self.snapshot(&id).await?;
91 let mark = if task.status == Status::Done {
92 "x"
93 } else {
94 " "
95 };
96 out.push_str(&" ".repeat(depth));
97 out.push_str(&format!("- [{mark}] {}\n", task.title));
98 for link in self.children_of(&id).await.into_iter().rev() {
100 stack.push((link.to, depth + 1));
101 }
102 }
103 Ok(out)
104 }
105
106 pub async fn import_md(&self, md: &str) -> Result<Vec<TaskSnapshot>, Error> {
109 let mut roots = Vec::new();
110 let mut stack: Vec<Id> = Vec::new();
111 for raw in md.lines() {
112 let Some((depth, mark, title)) = parse_md_line(raw) else {
113 continue;
114 };
115 stack.truncate(depth);
116 let parent = stack.last().cloned();
117 let status = if mark == 'x' {
118 Status::Done
119 } else {
120 Status::Todo
121 };
122 let task = self.create(title, parent.as_ref(), status, []).await?;
123 if parent.is_none() {
124 roots.push(task.clone());
125 }
126 stack.push(task.id.clone());
127 }
128 Ok(roots)
129 }
130}
131
132fn parse_md_line(line: &str) -> Option<(usize, char, &str)> {
134 let indent = line.len() - line.trim_start().len();
135 let depth = indent / 2;
136 let rest = line.trim_start();
137 let rest = rest.strip_prefix("- [")?;
138 let mark = rest.chars().next()?;
139 let title = rest.get(1..)?.strip_prefix("] ")?.trim();
140 Some((depth, mark, title))
141}