| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
use chrono::{DateTime, NaiveDate, TimeZone, Utc}; |
| 8 |
use goingson_core::{Priority, ProjectId, Task, TaskId}; |
| 9 |
use kberg::Error; |
| 10 |
use serde_json::{Value, json}; |
| 11 |
use uuid::Uuid; |
| 12 |
|
| 13 |
|
| 14 |
pub fn req_str<'a>(tool: &str, args: &'a Value, field: &str) -> Result<&'a str, Error> { |
| 15 |
args.get(field) |
| 16 |
.and_then(Value::as_str) |
| 17 |
.filter(|s| !s.trim().is_empty()) |
| 18 |
.ok_or_else(|| Error::InvalidArgs { |
| 19 |
tool: tool.to_string(), |
| 20 |
message: format!("missing or empty string field `{field}`"), |
| 21 |
}) |
| 22 |
} |
| 23 |
|
| 24 |
|
| 25 |
pub fn parse_task_id(tool: &str, s: &str) -> Result<TaskId, Error> { |
| 26 |
Uuid::parse_str(s.trim()) |
| 27 |
.map(TaskId::from_uuid) |
| 28 |
.map_err(|_| Error::InvalidArgs { |
| 29 |
tool: tool.to_string(), |
| 30 |
message: format!("`{s}` is not a valid task id (expected a UUID)"), |
| 31 |
}) |
| 32 |
} |
| 33 |
|
| 34 |
|
| 35 |
|
| 36 |
|
| 37 |
|
| 38 |
pub fn parse_due(tool: &str, value: Option<&Value>) -> Result<Option<DateTime<Utc>>, Error> { |
| 39 |
let Some(raw) = value.and_then(Value::as_str) else { |
| 40 |
return Ok(None); |
| 41 |
}; |
| 42 |
let raw = raw.trim(); |
| 43 |
if raw.is_empty() { |
| 44 |
return Ok(None); |
| 45 |
} |
| 46 |
if let Ok(dt) = DateTime::parse_from_rfc3339(raw) { |
| 47 |
return Ok(Some(dt.with_timezone(&Utc))); |
| 48 |
} |
| 49 |
if let Ok(date) = NaiveDate::parse_from_str(raw, "%Y-%m-%d") { |
| 50 |
let naive = date.and_hms_opt(0, 0, 0).expect("midnight is always valid"); |
| 51 |
return Ok(Some(Utc.from_utc_datetime(&naive))); |
| 52 |
} |
| 53 |
Err(Error::InvalidArgs { |
| 54 |
tool: tool.to_string(), |
| 55 |
message: format!("`{raw}` is not an RFC 3339 timestamp or YYYY-MM-DD date"), |
| 56 |
}) |
| 57 |
} |
| 58 |
|
| 59 |
|
| 60 |
|
| 61 |
|
| 62 |
pub fn parse_priority(value: Option<&Value>) -> Priority { |
| 63 |
match value.and_then(Value::as_str) { |
| 64 |
Some(s) => Priority::from_str_or_default(s), |
| 65 |
None => Priority::default(), |
| 66 |
} |
| 67 |
} |
| 68 |
|
| 69 |
|
| 70 |
pub fn parse_tags(value: Option<&Value>) -> Vec<String> { |
| 71 |
value |
| 72 |
.and_then(Value::as_array) |
| 73 |
.map(|arr| { |
| 74 |
arr.iter() |
| 75 |
.filter_map(Value::as_str) |
| 76 |
.map(str::to_string) |
| 77 |
.collect() |
| 78 |
}) |
| 79 |
.unwrap_or_default() |
| 80 |
} |
| 81 |
|
| 82 |
|
| 83 |
|
| 84 |
pub fn source_tag(source: &str) -> String { |
| 85 |
format!("source:{source}") |
| 86 |
} |
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
pub fn priority_word(p: &Priority) -> &'static str { |
| 91 |
match p { |
| 92 |
Priority::High => "High", |
| 93 |
Priority::Medium => "Medium", |
| 94 |
Priority::Low => "Low", |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
|
| 99 |
pub fn task_row(t: &Task) -> Value { |
| 100 |
json!({ |
| 101 |
"id": t.id.to_string(), |
| 102 |
"description": t.description, |
| 103 |
"status": t.status.as_str(), |
| 104 |
"priority": priority_word(&t.priority), |
| 105 |
"due": t.due.map(|d| d.to_rfc3339()), |
| 106 |
"tags": t.tags, |
| 107 |
"project": t.project_name, |
| 108 |
"project_id": t.project_id.map(|p: ProjectId| p.to_string()), |
| 109 |
"status_tokens": t.status_tokens.iter() |
| 110 |
.map(|k| json!({ |
| 111 |
"kind": k.kind, |
| 112 |
"reference": k.reference, |
| 113 |
"state": k.state.as_str(), |
| 114 |
"primary": k.is_primary, |
| 115 |
})) |
| 116 |
.collect::<Vec<_>>(), |
| 117 |
}) |
| 118 |
} |
| 119 |
|