use std::sync::Arc;
use anyhow::{Context, Result};
use chrono::Utc;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel_async::{RunQueryDsl, sync_connection_wrapper::SyncConnectionWrapper};
use tokio::sync::Mutex;
use crate::database::models::todos::{NewTodo, Todo};
#[derive(Clone)]
pub struct TodoRepository {
conn: Arc<Mutex<SyncConnectionWrapper<SqliteConnection>>>,
}
impl TodoRepository {
pub fn new(conn: SyncConnectionWrapper<SqliteConnection>) -> Self {
Self {
conn: Arc::new(Mutex::new(conn)),
}
}
pub async fn list_all(&self) -> Result<Vec<Todo>> {
use crate::schema::todos::dsl::*;
let mut conn = self.conn.lock().await;
let records = todos
.order(created_at.desc())
.select(Todo::as_select())
.load(&mut *conn)
.await
.context("failed to load todos")?;
Ok(records)
}
pub async fn insert(&self, draft: &NewTodoDraft) -> Result<Todo> {
use crate::schema::todos::dsl::*;
let mut conn = self.conn.lock().await;
let inserted = diesel::insert_into(todos)
.values(draft.as_insertable())
.returning(Todo::as_returning())
.get_result(&mut *conn)
.await
.context("failed to insert todo")?;
Ok(inserted)
}
pub async fn set_completed(&self, todo_id: i32, completed_state: bool) -> Result<()> {
use crate::schema::todos::dsl::*;
let mut conn = self.conn.lock().await;
let timestamp = Utc::now().naive_utc();
diesel::update(todos.filter(id.eq(todo_id)))
.set((completed.eq(completed_state), updated_at.eq(timestamp)))
.execute(&mut *conn)
.await
.with_context(|| format!("failed to update todo #{todo_id}"))?;
Ok(())
}
pub async fn delete(&self, todo_id: i32) -> Result<()> {
use crate::schema::todos::dsl::*;
let mut conn = self.conn.lock().await;
diesel::delete(todos.filter(id.eq(todo_id)))
.execute(&mut *conn)
.await
.with_context(|| format!("failed to delete todo #{todo_id}"))?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Priority {
Low,
Medium,
High,
}
impl Priority {
pub const fn as_str(self) -> &'static str {
match self {
Priority::Low => "low",
Priority::Medium => "medium",
Priority::High => "high",
}
}
pub const fn label(self) -> &'static str {
match self {
Priority::Low => "Low",
Priority::Medium => "Medium",
Priority::High => "High",
}
}
pub const fn increase(self) -> Self {
match self {
Priority::Low => Priority::Medium,
Priority::Medium => Priority::High,
Priority::High => Priority::High,
}
}
pub const fn decrease(self) -> Self {
match self {
Priority::Low => Priority::Low,
Priority::Medium => Priority::Low,
Priority::High => Priority::Medium,
}
}
pub const fn cycle(self) -> Self {
match self {
Priority::Low => Priority::Medium,
Priority::Medium => Priority::High,
Priority::High => Priority::Low,
}
}
}
impl Default for Priority {
fn default() -> Self {
Priority::Medium
}
}
#[derive(Clone, Debug, Default)]
pub struct NewTodoDraft {
pub title: String,
pub description: String,
pub priority: Priority,
}
impl NewTodoDraft {
pub fn is_valid(&self) -> bool {
!self.title.trim().is_empty()
}
pub fn clear(&mut self) {
self.title.clear();
self.description.clear();
self.priority = Priority::Medium;
}
pub fn as_insertable(&self) -> NewTodo<'_> {
NewTodo {
title: self.title.trim(),
description: self.description_field(),
priority: self.priority.as_str(),
}
}
fn description_field(&self) -> Option<&str> {
let desc = self.description.trim();
if desc.is_empty() { None } else { Some(desc) }
}
}