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::{Project, 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
132pub fn map_project_row(row: &SqliteRow) -> Project {
134 Project {
135 uuid: parse_uuid_with_fallback(&row.get::<String, _>("uuid")),
136 title: row.get("title"),
137 status: match row.get::<i32, _>("status") {
138 1 => TaskStatus::Completed,
139 2 => TaskStatus::Canceled,
140 3 => TaskStatus::Trashed,
141 _ => TaskStatus::Incomplete,
142 },
143 area_uuid: parse_optional_uuid(row.get::<Option<String>, _>("area")),
144 notes: row.get("notes"),
145 deadline: row
146 .get::<Option<i64>, _>("deadline")
147 .and_then(|ts| DateTime::from_timestamp(ts, 0))
148 .map(|dt| dt.date_naive()),
149 start_date: row
150 .get::<Option<i64>, _>("startDate")
151 .and_then(|ts| DateTime::from_timestamp(ts, 0))
152 .map(|dt| dt.date_naive()),
153 tags: Vec::new(),
154 tasks: Vec::new(),
155 created: {
156 let ts = safe_timestamp_convert(row.get::<f64, _>("creationDate"));
157 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
158 },
159 modified: {
160 let ts = safe_timestamp_convert(row.get::<f64, _>("userModificationDate"));
161 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
162 },
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_parse_uuid_with_fallback_standard() {
172 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
173 let uuid = parse_uuid_with_fallback(uuid_str);
174 assert_eq!(uuid.to_string(), uuid_str);
175 }
176
177 #[test]
178 fn test_parse_uuid_with_fallback_things_format() {
179 let things_id = "ABC123XYZ";
181 let uuid1 = parse_uuid_with_fallback(things_id);
182 let uuid2 = parse_uuid_with_fallback(things_id);
183 assert_eq!(uuid1, uuid2);
185 }
186
187 #[test]
188 fn test_parse_optional_uuid_some() {
189 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
190 let result = parse_optional_uuid(Some(uuid_str.to_string()));
191 assert!(result.is_some());
192 assert_eq!(result.unwrap().to_string(), uuid_str);
193 }
194
195 #[test]
196 fn test_parse_optional_uuid_none() {
197 let result = parse_optional_uuid(None);
198 assert!(result.is_none());
199 }
200}