Skip to main content

boarddown_core/parser/
ast.rs

1use boarddown_schema::{Board, BoardId, Column, ColumnRef, Status, Task, TaskId, Metadata, BoardMetadata};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone)]
5pub struct BoardAst {
6    pub frontmatter: Option<serde_yaml::Value>,
7    pub sections: Vec<Section>,
8    pub tasks: Vec<TaskNode>,
9}
10
11#[derive(Debug, Clone)]
12pub enum Section {
13    Meta(MetaSection),
14    Column(ColumnSection),
15    Custom { heading: String, content: String },
16}
17
18#[derive(Debug, Clone)]
19pub struct MetaSection {
20    pub fields: Vec<(String, String)>,
21}
22
23#[derive(Debug, Clone)]
24pub struct ColumnSection {
25    pub name: String,
26    pub order: usize,
27}
28
29#[derive(Debug, Clone)]
30pub struct TaskNode {
31    pub id: Option<TaskId>,
32    pub title: String,
33    pub status: Status,
34    pub metadata: String,
35    pub indent: usize,
36    pub line_number: usize,
37}
38
39impl BoardAst {
40    pub fn into_board(self, id: BoardId) -> Result<Board, crate::Error> {
41        let mut columns: Vec<Column> = Vec::new();
42        let mut tasks: HashMap<TaskId, Task> = HashMap::new();
43        let mut current_column = "Todo".to_string();
44        let mut task_counter: u64 = 0;
45        let mut board_metadata = BoardMetadata::default();
46        let mut title = String::new();
47        
48        if let Some(ref fm) = self.frontmatter {
49            if let serde_yaml::Value::Mapping(map) = fm {
50                if let Some(serde_yaml::Value::String(t)) = map.get(&serde_yaml::Value::String("board".to_string())) {
51                    title = t.clone();
52                }
53                if let Some(serde_yaml::Value::String(prefix)) = map.get(&serde_yaml::Value::String("id-prefix".to_string())) {
54                    board_metadata.id_prefix = Some(prefix.clone());
55                }
56            }
57        }
58        
59        for section in &self.sections {
60            if let Section::Column(col_section) = section {
61                if !columns.iter().any(|c| c.name == col_section.name) {
62                    columns.push(Column::new(&col_section.name));
63                }
64            }
65        }
66        
67        let mut prev_task_id: Option<TaskId> = None;
68        let mut pending_metadata: HashMap<String, String> = HashMap::new();
69        
70        for task_node in self.tasks {
71            if let Some(ref prev_id) = prev_task_id {
72                apply_metadata_to_task(&mut tasks, prev_id, &pending_metadata);
73            }
74            pending_metadata.clear();
75            
76            let column = current_column.clone();
77            let task_id = task_node.id.unwrap_or_else(|| {
78                task_counter += 1;
79                let prefix = board_metadata.id_prefix.as_deref().unwrap_or("TASK");
80                TaskId::new(prefix, task_counter)
81            });
82            
83            current_column = column.clone();
84            
85            let task = Task {
86                id: task_id.clone(),
87                title: task_node.title,
88                status: task_node.status,
89                column: ColumnRef::Name(column),
90                metadata: Metadata::default(),
91                dependencies: Vec::new(),
92                references: Vec::new(),
93                created_at: chrono::Utc::now(),
94                updated_at: chrono::Utc::now(),
95            };
96            
97            tasks.insert(task_id.clone(), task);
98            prev_task_id = Some(task_id);
99        }
100        
101        if let Some(ref prev_id) = prev_task_id {
102            apply_metadata_to_task(&mut tasks, prev_id, &pending_metadata);
103        }
104        
105        if columns.is_empty() {
106            columns.push(Column::new("Todo"));
107        }
108        
109        Ok(Board {
110            id,
111            title,
112            columns,
113            tasks,
114            metadata: board_metadata,
115            created_at: chrono::Utc::now(),
116            updated_at: chrono::Utc::now(),
117        })
118    }
119}
120
121fn apply_metadata_to_task(
122    tasks: &mut HashMap<TaskId, Task>,
123    task_id: &TaskId,
124    metadata: &HashMap<String, String>,
125) {
126    if let Some(task) = tasks.get_mut(task_id) {
127        for (key, value) in metadata {
128            match key.as_str() {
129                "Tags" => {
130                    task.metadata.tags = value.split(',')
131                        .map(|s| s.trim().to_string())
132                        .filter(|s| !s.is_empty())
133                        .collect();
134                }
135                "Assign" => {
136                    task.metadata.assign = Some(value.clone());
137                }
138                "Estimate" => {
139                    task.metadata.estimate = value.parse().ok();
140                }
141                "Priority" => {
142                    task.metadata.priority = value.parse().ok();
143                }
144                "Due" => {
145                    task.metadata.due = chrono::DateTime::parse_from_rfc3339(value)
146                        .ok()
147                        .map(|dt| dt.with_timezone(&chrono::Utc));
148                }
149                "Started" => {
150                    task.metadata.started = chrono::DateTime::parse_from_rfc3339(value)
151                        .ok()
152                        .map(|dt| dt.with_timezone(&chrono::Utc));
153                }
154                "Closed" => {
155                    task.metadata.closed = chrono::DateTime::parse_from_rfc3339(value)
156                        .ok()
157                        .map(|dt| dt.with_timezone(&chrono::Utc));
158                }
159                "Depends" => {
160                    task.dependencies = value.split(',')
161                        .map(|s| s.trim())
162                        .filter(|s| !s.is_empty())
163                        .map(|s| {
164                            if s.starts_with('{') && s.ends_with('}') {
165                                &s[1..s.len()-1]
166                            } else {
167                                s
168                            }
169                        })
170                        .map(|s| TaskId::from(s))
171                        .collect();
172                }
173                "Refs" => {
174                    task.references = value.split(',')
175                        .map(|s| s.trim())
176                        .filter(|s| !s.is_empty())
177                        .map(|s| {
178                            if s.starts_with('{') && s.ends_with('}') {
179                                &s[1..s.len()-1]
180                            } else {
181                                s
182                            }
183                        })
184                        .map(|s| TaskId::from(s))
185                        .collect();
186                }
187                "Branch" => {
188                    task.metadata.branch = Some(value.clone());
189                }
190                "PR" => {
191                    task.metadata.pr = Some(value.clone());
192                }
193                "Epic" => {
194                    task.metadata.epic = Some(value.clone());
195                }
196                _ => {
197                    task.metadata.custom.insert(key.clone(), serde_json::Value::String(value.clone()));
198                }
199            }
200        }
201    }
202}