Skip to main content

boarddown_schema/
metadata.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use ts_rs::TS;
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
6#[ts(export)]
7pub struct Metadata {
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub assign: Option<String>,
10    #[serde(skip_serializing_if = "Vec::is_empty", default)]
11    pub tags: Vec<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub priority: Option<Priority>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub estimate: Option<f32>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub due: Option<chrono::DateTime<chrono::Utc>>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub started: Option<chrono::DateTime<chrono::Utc>>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub closed: Option<chrono::DateTime<chrono::Utc>>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub epic: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub branch: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub pr: Option<String>,
28    #[serde(default)]
29    #[ts(type = "Record<string, unknown>")]
30    pub custom: HashMap<String, serde_json::Value>,
31}
32
33impl PartialEq for Metadata {
34    fn eq(&self, other: &Self) -> bool {
35        self.assign == other.assign
36            && self.tags == other.tags
37            && self.priority == other.priority
38            && self.estimate.map(|e| e.to_bits()) == other.estimate.map(|e| e.to_bits())
39            && self.due == other.due
40            && self.started == other.started
41            && self.closed == other.closed
42            && self.epic == other.epic
43            && self.branch == other.branch
44            && self.pr == other.pr
45            && self.custom == other.custom
46    }
47}
48
49impl Eq for Metadata {}
50
51impl Metadata {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    pub fn with(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
57        match key {
58            "assign" => self.assign = value.into().as_str().map(String::from),
59            "tags" => {
60                if let serde_json::Value::Array(arr) = value.into() {
61                    self.tags = arr
62                        .into_iter()
63                        .filter_map(|v| v.as_str().map(String::from))
64                        .collect();
65                }
66            }
67            "priority" => {
68                self.priority = serde_json::from_value(value.into()).ok();
69            }
70            "estimate" => {
71                self.estimate = value.into().as_f64().map(|v| v as f32);
72            }
73            other => {
74                self.custom.insert(other.to_string(), value.into());
75            }
76        }
77        self
78    }
79
80    pub fn get(&self, key: &str) -> Option<serde_json::Value> {
81        match key {
82            "assign" => self.assign.as_ref().map(|s| serde_json::Value::String(s.clone())),
83            "tags" => Some(serde_json::Value::Array(
84                self.tags.iter().map(|s| serde_json::Value::String(s.clone())).collect(),
85            )),
86            "priority" => self.priority.map(|p| serde_json::to_value(p).unwrap()),
87            "estimate" => self.estimate.map(|e| {
88                serde_json::Number::from_f64(e as f64)
89                    .map(serde_json::Value::Number)
90                    .unwrap_or(serde_json::Value::Null)
91            }),
92            other => self.custom.get(other).cloned(),
93        }
94    }
95
96    pub fn remove(&mut self, key: &str) {
97        match key {
98            "assign" => self.assign = None,
99            "tags" => self.tags.clear(),
100            "priority" => self.priority = None,
101            "estimate" => self.estimate = None,
102            "due" => self.due = None,
103            "started" => self.started = None,
104            "closed" => self.closed = None,
105            "epic" => self.epic = None,
106            "branch" => self.branch = None,
107            "pr" => self.pr = None,
108            other => { self.custom.remove(other); }
109        }
110    }
111
112    pub fn set(&mut self, key: &str, value: serde_json::Value) {
113        match key {
114            "assign" => self.assign = value.as_str().map(String::from),
115            "tags" => {
116                if let serde_json::Value::Array(arr) = value {
117                    self.tags = arr
118                        .into_iter()
119                        .filter_map(|v| v.as_str().map(String::from))
120                        .collect();
121                }
122            }
123            "priority" => {
124                self.priority = serde_json::from_value(value).ok();
125            }
126            "estimate" => {
127                self.estimate = value.as_f64().map(|v| v as f32);
128            }
129            "due" => {
130                self.due = serde_json::from_value(value).ok();
131            }
132            "started" => {
133                self.started = serde_json::from_value(value).ok();
134            }
135            "closed" => {
136                self.closed = serde_json::from_value(value).ok();
137            }
138            "epic" => {
139                self.epic = value.as_str().map(String::from);
140            }
141            "branch" => {
142                self.branch = value.as_str().map(String::from);
143            }
144            "pr" => {
145                self.pr = value.as_str().map(String::from);
146            }
147            other => {
148                self.custom.insert(other.to_string(), value);
149            }
150        }
151    }
152
153    pub fn merge(&mut self, other: &Metadata) {
154        if other.assign.is_some() {
155            self.assign = other.assign.clone();
156        }
157        if !other.tags.is_empty() {
158            for tag in &other.tags {
159                if !self.tags.contains(tag) {
160                    self.tags.push(tag.clone());
161                }
162            }
163        }
164        if other.priority.is_some() {
165            self.priority = other.priority;
166        }
167        if other.estimate.is_some() {
168            self.estimate = other.estimate;
169        }
170        if other.due.is_some() {
171            self.due = other.due;
172        }
173        if other.started.is_some() {
174            self.started = other.started;
175        }
176        if other.closed.is_some() {
177            self.closed = other.closed;
178        }
179        if other.epic.is_some() {
180            self.epic = other.epic.clone();
181        }
182        if other.branch.is_some() {
183            self.branch = other.branch.clone();
184        }
185        if other.pr.is_some() {
186            self.pr = other.pr.clone();
187        }
188        for (k, v) in &other.custom {
189            self.custom.insert(k.clone(), v.clone());
190        }
191    }
192
193    pub fn iter(&self) -> impl Iterator<Item = (String, serde_json::Value)> + '_ {
194        let standard: Vec<(String, serde_json::Value)> = [
195            self.assign.as_ref().map(|s| ("assign".to_string(), serde_json::Value::String(s.clone()))),
196            if !self.tags.is_empty() {
197                Some(("tags".to_string(), serde_json::Value::Array(
198                    self.tags.iter().map(|s| serde_json::Value::String(s.clone())).collect(),
199                )))
200            } else { None },
201            self.priority.map(|p| ("priority".to_string(), serde_json::to_value(p).unwrap())),
202            self.estimate.map(|e| ("estimate".to_string(), 
203                serde_json::Number::from_f64(e as f64)
204                    .map(serde_json::Value::Number)
205                    .unwrap_or(serde_json::Value::Null))),
206            self.due.map(|d| ("due".to_string(), serde_json::to_value(d).unwrap())),
207        ].into_iter().flatten().collect();
208        
209        standard.into_iter().chain(
210            self.custom.iter().map(|(k, v)| (k.clone(), v.clone()))
211        )
212    }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
216#[ts(export)]
217#[serde(rename_all = "lowercase")]
218pub enum Priority {
219    Low,
220    Medium,
221    High,
222    Critical,
223}
224
225impl Default for Priority {
226    fn default() -> Self {
227        Self::Medium
228    }
229}
230
231impl std::str::FromStr for Priority {
232    type Err = String;
233    
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        match s.to_lowercase().as_str() {
236            "low" => Ok(Self::Low),
237            "medium" => Ok(Self::Medium),
238            "high" => Ok(Self::High),
239            "critical" => Ok(Self::Critical),
240            _ => Err(format!("Invalid priority: {}", s)),
241        }
242    }
243}