1use anyhow::{anyhow, Result};
2use serde::Deserialize;
3use std::io::BufRead;
4use std::path::Path;
5
6use crate::db;
7use crate::model::{Effort, Priority, Status, TaskId};
8
9#[derive(Deserialize)]
10struct ImportTask {
11 id: String,
12 title: String,
13 #[serde(default)]
14 description: String,
15 #[serde(rename = "type", default = "default_type")]
16 task_type: String,
17 #[serde(default = "default_priority")]
18 priority: String,
19 #[serde(default = "default_status")]
20 status: String,
21 #[serde(default = "default_effort")]
22 effort: String,
23 #[serde(default)]
24 parent: Option<String>,
25 created_at: String,
26 updated_at: String,
27 #[serde(default)]
28 deleted_at: Option<String>,
29 #[serde(default)]
30 labels: Vec<String>,
31 #[serde(default)]
32 blockers: Vec<String>,
33 #[serde(default)]
34 logs: Vec<ImportLog>,
35}
36
37#[derive(Deserialize)]
38struct ImportLog {
39 id: String,
40 timestamp: String,
41 message: String,
42}
43
44fn default_type() -> String {
45 "task".into()
46}
47fn default_priority() -> String {
48 "medium".into()
49}
50fn default_status() -> String {
51 "open".into()
52}
53fn default_effort() -> String {
54 "medium".into()
55}
56
57pub fn run(root: &Path, file: &str) -> Result<()> {
58 let store = db::open(root)?;
59
60 let reader: Box<dyn BufRead> = if file == "-" {
61 Box::new(std::io::stdin().lock())
62 } else {
63 Box::new(std::io::BufReader::new(std::fs::File::open(file)?))
64 };
65
66 for line in reader.lines() {
67 let line = line?;
68 if line.trim().is_empty() {
69 continue;
70 }
71 let t: ImportTask = serde_json::from_str(&line)?;
72 let id = TaskId::parse(&t.id)?;
73 Priority::parse(&t.priority)?;
74 Status::parse(&t.status)?;
75 Effort::parse(&t.effort)?;
76
77 store.apply_and_persist(|doc| {
78 let tasks = doc.get_map("tasks");
79 let task = if let Some(existing) = db::get_task_map(&tasks, &id)? {
80 existing
81 } else {
82 db::insert_task_map(&tasks, &id)?
83 };
84
85 task.insert("title", t.title.clone())?;
86 task.insert("description", t.description.clone())?;
87 task.insert("type", t.task_type.clone())?;
88 task.insert("priority", t.priority.clone())?;
89 task.insert("status", t.status.clone())?;
90 task.insert("effort", t.effort.clone())?;
91 task.insert("parent", t.parent.as_deref().unwrap_or(""))?;
92 task.insert("created_at", t.created_at.clone())?;
93 task.insert("updated_at", t.updated_at.clone())?;
94 task.insert("deleted_at", t.deleted_at.as_deref().unwrap_or(""))?;
95
96 let labels = db::get_or_create_child_map(&task, "labels")?;
97 for lbl in &t.labels {
98 labels.insert(lbl, true)?;
99 }
100 let blockers = db::get_or_create_child_map(&task, "blockers")?;
101 for blk in &t.blockers {
102 let parsed =
103 TaskId::parse(blk).map_err(|_| anyhow!("invalid blocker id '{blk}'"))?;
104 blockers.insert(parsed.as_str(), true)?;
105 }
106 let logs = db::get_or_create_child_map(&task, "logs")?;
107 for entry in &t.logs {
108 let log_id = TaskId::parse(&entry.id)
109 .map_err(|_| anyhow!("invalid log id '{}'", entry.id))?;
110 let record = logs.get_or_create_container(log_id.as_str(), loro::LoroMap::new())?;
111 record.insert("timestamp", entry.timestamp.clone())?;
112 record.insert("message", entry.message.clone())?;
113 }
114 Ok(())
115 })?;
116 }
117
118 Ok(())
119}