import.rs

  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}