things3_core/database/
mappers.rs1use crate::{
7 database::{safe_timestamp_convert, things_date_to_naive_date, things_uuid_to_uuid},
8 error::Result as ThingsResult,
9 models::{Task, TaskStatus, TaskType},
10};
11use chrono::{DateTime, Utc};
12use sqlx::sqlite::SqliteRow;
13use sqlx::Row;
14use uuid::Uuid;
15
16pub fn parse_uuid_with_fallback(uuid_str: &str) -> Uuid {
21 Uuid::parse_str(uuid_str).unwrap_or_else(|_| things_uuid_to_uuid(uuid_str))
22}
23
24pub fn parse_optional_uuid(opt_str: Option<String>) -> Option<Uuid> {
29 opt_str.map(|s| {
30 Uuid::parse_str(&s)
31 .ok()
32 .unwrap_or_else(|| things_uuid_to_uuid(&s))
33 })
34}
35
36pub fn map_task_row(row: &SqliteRow) -> ThingsResult<Task> {
45 let uuid_str: String = row.get("uuid");
46 let uuid = parse_uuid_with_fallback(&uuid_str);
47
48 let title: String = row.get("title");
49
50 let status_i32: i32 = row.get("status");
51 let status = match status_i32 {
52 1 => TaskStatus::Completed,
53 2 => TaskStatus::Canceled,
54 3 => TaskStatus::Trashed,
55 _ => TaskStatus::Incomplete,
56 };
57
58 let type_i32: i32 = row.get("type");
59 let task_type = match type_i32 {
60 1 => TaskType::Project,
61 2 => TaskType::Heading,
62 _ => TaskType::Todo,
63 };
64
65 let notes: Option<String> = row.get("notes");
66
67 let start_date = row
68 .get::<Option<i64>, _>("startDate")
69 .and_then(things_date_to_naive_date);
70
71 let deadline = row
72 .get::<Option<i64>, _>("deadline")
73 .and_then(things_date_to_naive_date);
74
75 let creation_ts: f64 = row.get("creationDate");
76 let created = {
77 let ts = safe_timestamp_convert(creation_ts);
78 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
79 };
80
81 let modification_ts: f64 = row.get("userModificationDate");
82 let modified = {
83 let ts = safe_timestamp_convert(modification_ts);
84 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
85 };
86
87 let stop_date = row.get::<Option<f64>, _>("stopDate").and_then(|ts| {
88 let ts_i64 = safe_timestamp_convert(ts);
89 DateTime::from_timestamp(ts_i64, 0)
90 });
91
92 let project_uuid = row
93 .get::<Option<String>, _>("project")
94 .map(|s| parse_uuid_with_fallback(&s));
95
96 let area_uuid = row
97 .get::<Option<String>, _>("area")
98 .map(|s| parse_uuid_with_fallback(&s));
99
100 let parent_uuid = row
101 .get::<Option<String>, _>("heading")
102 .map(|s| parse_uuid_with_fallback(&s));
103
104 let tags = row
106 .get::<Option<Vec<u8>>, _>("cachedTags")
107 .and_then(|blob| {
108 crate::database::deserialize_tags_from_blob(&blob).ok()
110 })
111 .unwrap_or_default();
112
113 Ok(Task {
114 uuid,
115 title,
116 status,
117 task_type,
118 notes,
119 start_date,
120 deadline,
121 created,
122 modified,
123 stop_date,
124 project_uuid,
125 area_uuid,
126 parent_uuid,
127 tags,
128 children: Vec::new(),
129 })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_parse_uuid_with_fallback_standard() {
138 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
139 let uuid = parse_uuid_with_fallback(uuid_str);
140 assert_eq!(uuid.to_string(), uuid_str);
141 }
142
143 #[test]
144 fn test_parse_uuid_with_fallback_things_format() {
145 let things_id = "ABC123XYZ";
147 let uuid1 = parse_uuid_with_fallback(things_id);
148 let uuid2 = parse_uuid_with_fallback(things_id);
149 assert_eq!(uuid1, uuid2);
151 }
152
153 #[test]
154 fn test_parse_optional_uuid_some() {
155 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
156 let result = parse_optional_uuid(Some(uuid_str.to_string()));
157 assert!(result.is_some());
158 assert_eq!(result.unwrap().to_string(), uuid_str);
159 }
160
161 #[test]
162 fn test_parse_optional_uuid_none() {
163 let result = parse_optional_uuid(None);
164 assert!(result.is_none());
165 }
166}