1use std::error::Error;
6use std::path::PathBuf;
7
8use aimcal_ical::{CalendarComponent, ICalendar};
9use jiff::Zoned;
10use tokio::fs;
11use uuid::Uuid;
12
13use crate::io::{add_calendar, parse_ics, write_ics};
14use crate::localdb::LocalDb;
15use crate::short_id::ShortIds;
16use crate::{
17 Config, Event, EventConditions, EventDraft, EventPatch, Id, Kind, Pager, Todo, TodoConditions,
18 TodoDraft, TodoPatch, TodoSort,
19};
20
21#[derive(Debug, Clone)]
23pub struct Aim {
24 now: Zoned,
25 config: Config,
26 db: LocalDb,
27 short_ids: ShortIds,
28 calendar_path: PathBuf,
29}
30
31impl Aim {
32 pub async fn new(mut config: Config) -> Result<Self, Box<dyn Error>> {
37 let now = Zoned::now();
38
39 config.normalize()?;
40 prepare(&config).await?;
41
42 let db = initialize_db(&config).await?;
43 let short_ids = ShortIds::new(db.clone());
44 let calendar_path = config.calendar_path.clone();
45 add_calendar(&db, &calendar_path)
46 .await
47 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
48
49 Ok(Self {
50 now,
51 config,
52 db,
53 short_ids,
54 calendar_path,
55 })
56 }
57
58 #[must_use]
60 pub fn now(&self) -> Zoned {
61 self.now.clone()
62 }
63
64 pub fn refresh_now(&mut self) {
66 self.now = Zoned::now();
67 }
68
69 #[must_use]
71 pub fn default_event_draft(&self) -> EventDraft {
72 EventDraft::default(&self.now)
73 }
74
75 pub async fn get_event(&self, id: &Id) -> Result<impl Event + 'static, Box<dyn Error>> {
80 let uid = self.short_ids.get_uid(id).await?;
81 match self.db.events.get(&uid).await {
82 Ok(Some(event)) => Ok(self.short_ids.event(event).await?),
83 Ok(None) => Err("Event not found".into()),
84 Err(e) => Err(e.into()),
85 }
86 }
87
88 pub async fn new_event(
93 &self,
94 draft: EventDraft,
95 ) -> Result<impl Event + 'static, Box<dyn Error>> {
96 let uid = self.generate_uid(Kind::Event).await?;
97 let event = draft.resolve(&self.now).into_ics(&uid);
98 let path = self.get_path(&uid);
99
100 let mut calendar = ICalendar::new();
103 calendar.components.push(event.clone().into());
104
105 write_ics(&path, &calendar).await?;
106 self.db.upsert_event(&path, &event).await?;
107
108 let event = self.short_ids.event(event).await?;
109 Ok(event)
110 }
111
112 pub async fn update_event(
117 &self,
118 id: &Id,
119 patch: EventPatch,
120 ) -> Result<impl Event + 'static, Box<dyn Error>> {
121 let uid = self.short_ids.get_uid(id).await?;
122 let Some(event) = self.db.events.get(&uid).await? else {
123 return Err("Event not found".into());
124 };
125
126 let path: PathBuf = event.path().into();
127 let mut calendar = parse_ics(&path).await?;
128
129 let mut found = false;
131 for component in &mut calendar.components {
132 if let CalendarComponent::Event(e) = component
133 && e.uid.content.to_string() == event.uid()
134 {
136 patch.resolve(self.now.clone()).apply_to(e);
137 found = true;
138 break;
139 }
140 }
141
142 if !found {
143 return Err("Event not found in calendar".into());
144 }
145
146 write_ics(&path, &calendar).await?;
147 self.db.upsert_event(&path, &event).await?;
148
149 let event_with_id = self.short_ids.event(event).await?;
150 Ok(event_with_id)
151 }
152
153 pub async fn get_kind(&self, id: &Id) -> Result<Kind, Box<dyn Error>> {
158 tracing::debug!(?id, "getting kind of id");
159 if let Some(data) = self.short_ids.get(id).await? {
160 return Ok(data.kind);
161 }
162
163 let uid = id.as_uid();
164
165 tracing::debug!(uid, "checking if id is an event");
166 if self.db.events.get(uid).await?.is_some() {
167 return Ok(Kind::Event);
168 }
169
170 tracing::debug!(uid, "checking if id is a todo");
171 if self.db.todos.get(uid).await?.is_some() {
172 return Ok(Kind::Todo);
173 }
174
175 Err("Id not found".into())
176 }
177
178 pub async fn list_events(
183 &self,
184 conds: &EventConditions,
185 pager: &Pager,
186 ) -> Result<Vec<impl Event + 'static>, Box<dyn Error>> {
187 let conds = conds.resolve(&self.now)?;
188 let events = self.db.events.list(&conds, pager).await?;
189 let events = self.short_ids.events(events).await?;
190 Ok(events)
191 }
192
193 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, Box<dyn Error>> {
198 let conds = conds.resolve(&self.now)?;
199 Ok(self.db.events.count(&conds).await?)
200 }
201
202 pub fn default_todo_draft(&self) -> Result<TodoDraft, String> {
207 TodoDraft::default(&self.config, &self.now)
208 }
209
210 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo + 'static, Box<dyn Error>> {
215 let uid = self.generate_uid(Kind::Todo).await?;
216 let todo = draft.resolve(&self.config, &self.now).into_ics(&uid);
217 let path = self.get_path(&uid);
218
219 let mut calendar = ICalendar::new();
221 calendar.components.push(todo.clone().into());
222
223 write_ics(&path, &calendar).await?;
224 self.db.upsert_todo(&path, &todo).await?;
225
226 let todo_with_id = self.short_ids.todo(todo).await?;
227 Ok(todo_with_id)
228 }
229
230 pub async fn update_todo(
235 &self,
236 id: &Id,
237 patch: TodoPatch,
238 ) -> Result<impl Todo + 'static, Box<dyn Error>> {
239 let uid = self.short_ids.get_uid(id).await?;
240 let Some(todo) = self.db.todos.get(&uid).await? else {
241 return Err("Todo not found".into());
242 };
243
244 let path: PathBuf = todo.path().into();
245 let mut calendar = parse_ics(&path).await?;
246
247 let mut found = false;
249 for component in &mut calendar.components {
250 if let CalendarComponent::Todo(t) = component
251 && t.uid.content.to_string() == todo.uid()
252 {
253 patch.resolve(&self.now).apply_to(t);
254 found = true;
255 break;
256 }
257 }
258
259 if !found {
260 return Err("Todo not found in calendar".into());
261 }
262
263 write_ics(&path, &calendar).await?;
264 self.db.upsert_todo(&path, &todo).await?;
265
266 let todo = self.short_ids.todo(todo).await?;
267 Ok(todo)
268 }
269
270 pub async fn get_todo(&self, id: &Id) -> Result<impl Todo + 'static, Box<dyn Error>> {
275 let uid = self.short_ids.get_uid(id).await?;
276 match self.db.todos.get(&uid).await {
277 Ok(Some(todo)) => Ok(self.short_ids.todo(todo).await?),
278 Ok(None) => Err("Event not found".into()),
279 Err(e) => Err(e.into()),
280 }
281 }
282
283 pub async fn list_todos(
288 &self,
289 conds: &TodoConditions,
290 sort: &[TodoSort],
291 pager: &Pager,
292 ) -> Result<Vec<impl Todo + 'static>, Box<dyn Error>> {
293 let conds = conds.resolve(&self.now)?;
294 let sort = TodoSort::resolve_vec(sort, &self.config);
295 let todos = self.db.todos.list(&conds, &sort, pager).await?;
296 let todos = self.short_ids.todos(todos).await?;
297 Ok(todos)
298 }
299
300 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, Box<dyn Error>> {
305 let conds = conds.resolve(&self.now)?;
306 Ok(self.db.todos.count(&conds).await?)
307 }
308
309 pub async fn flush_short_ids(&self) -> Result<(), Box<dyn Error>> {
314 self.short_ids.flush().await
315 }
316
317 pub async fn close(self) -> Result<(), Box<dyn Error>> {
322 self.db.close().await
323 }
324
325 async fn generate_uid(&self, kind: Kind) -> Result<String, Box<dyn Error>> {
326 for i in 0..16 {
327 let uid = Uuid::new_v4().to_string(); tracing::debug!(
329 ?uid,
330 attempt = i + 1,
331 "generated uid, checking for uniqueness"
332 );
333
334 let exists = match kind {
335 Kind::Event => self.db.events.get(&uid).await?.is_some(),
336 Kind::Todo => self.db.todos.get(&uid).await?.is_some(),
337 };
338 if exists {
339 tracing::debug!(uid, ?kind, "uid already exists in db");
340 continue;
341 }
342
343 let path = self.get_path(&uid);
344 if fs::try_exists(&path).await? {
345 tracing::debug!(uid, ?path, "uid already exists as a file");
346 continue;
347 }
348 return Ok(uid);
349 }
350
351 tracing::warn!("failed to generate a unique uid after multiple attempts");
352 Err("Failed to generate a unique UID after multiple attempts".into())
353 }
354
355 fn get_path(&self, uid: &str) -> PathBuf {
356 self.calendar_path.join(format!("{uid}.ics"))
357 }
358}
359
360async fn prepare(config: &Config) -> Result<(), Box<dyn Error>> {
361 if let Some(parent) = &config.state_dir {
362 tracing::debug!(path = %parent.display(), "ensuring state directory exists");
363 fs::create_dir_all(parent).await?;
364 }
365 Ok(())
366}
367
368async fn initialize_db(config: &Config) -> Result<LocalDb, Box<dyn Error>> {
369 const NAME: &str = "aim.db";
370 let db = if let Some(parent) = &config.state_dir {
371 LocalDb::open(Some(&parent.join(NAME))).await
372 } else {
373 LocalDb::open(None).await
374 }
375 .map_err(|e| format!("Failed to initialize db: {e}"))?;
376
377 Ok(db)
378}