import.rs

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