birthday 0.2.0

🎂 CLI tool to remember birthdays of people you know
Documentation
use std::path::{Path, PathBuf};

use anyhow::Result;
use chrono::{NaiveDate, NaiveDateTime};
use directories::ProjectDirs;
use rusqlite::Connection;

use crate::Birthday;

pub fn add(name: String, date: NaiveDate) -> Result<()> {
    let db = get_db()?;
    let timestamp = to_timestamp(date);
    db.execute(
        "INSERT INTO birthdays(name, date_timestamp) VALUES(?1, ?2)",
        (name, timestamp),
    )?;
    Ok(())
}

fn get_db_path() -> Result<PathBuf> {
    let mut data_dir = match std::env::var("BIRTHDAY_DATA") {
        Ok(path) => PathBuf::from(path),
        Err(_) => match ProjectDirs::from("", "", "birthday") {
            Some(project_dirs) => project_dirs.data_dir().to_owned(),
            None => Path::new("./").to_owned(),
        },
    };

    std::fs::create_dir_all(&data_dir)?;
    data_dir.push("test.db");
    Ok(data_dir)
}

pub fn get_all() -> Result<Vec<Birthday>> {
    let db = get_db()?;
    let mut statement = db.prepare("SELECT id, name, date_timestamp FROM birthdays")?;
    let birthday_iter = statement.query_map([], |row| {
        let id = row.get(0)?;
        let name = row.get(1)?;
        let timestamp = row.get(2)?;
        let date = from_timestamp(timestamp);
        Ok(Birthday { id, name, date })
    })?;
    let birthdays = birthday_iter.collect::<Result<Vec<Birthday>, rusqlite::Error>>()?;
    Ok(birthdays)
}

pub fn remove(id: i32) -> Result<Option<Birthday>> {
    let db = get_db()?;
    let mut statement =
        db.prepare("DELETE FROM birthdays WHERE id = :id RETURNING id, name, date_timestamp")?;
    let birthday_iter = statement.query_map(&[(":id", id.to_string().as_str())], |row| {
        let id = row.get(0)?;
        let name = row.get(1)?;
        let timestamp = row.get(2)?;
        let date = from_timestamp(timestamp);
        Ok(Birthday { id, name, date })
    })?;
    let birthdays = birthday_iter.collect::<Result<Vec<Birthday>, rusqlite::Error>>()?;
    if birthdays.is_empty() {
        Ok(None)
    } else {
        Ok(Some(birthdays[0].clone()))
    }
}

fn get_db() -> Result<Connection> {
    let db_path = get_db_path()?;
    let db = Connection::open(db_path)?;
    db.execute(
        "CREATE TABLE IF NOT EXISTS birthdays (
             id INTEGER PRIMARY KEY,
             name TEXT NOT NULL,
             date_timestamp INTEGER NOT NULL
         ) STRICT",
        (),
    )?;
    Ok(db)
}

fn to_timestamp(date: NaiveDate) -> i64 {
    date.and_hms_opt(0, 0, 0).unwrap().timestamp()
}

fn from_timestamp(timestamp: i64) -> NaiveDate {
    NaiveDateTime::from_timestamp_opt(timestamp, 0)
        .unwrap()
        .date()
}