use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Due {
pub date: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub datetime: Option<String>,
#[serde(default)]
pub is_recurring: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub string: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
}
impl Due {
pub fn from_date(date: impl Into<String>) -> Self {
Self {
date: date.into(),
datetime: None,
is_recurring: false,
string: None,
timezone: None,
lang: None,
}
}
pub fn from_datetime(date: impl Into<String>, datetime: impl Into<String>) -> Self {
Self {
date: date.into(),
datetime: Some(datetime.into()),
is_recurring: false,
string: None,
timezone: None,
lang: None,
}
}
pub fn as_naive_date(&self) -> Option<NaiveDate> {
NaiveDate::parse_from_str(&self.date, "%Y-%m-%d").ok()
}
pub fn has_time(&self) -> bool {
self.datetime.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Deadline {
pub date: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Duration {
pub amount: i32,
pub unit: DurationUnit,
}
impl Duration {
pub fn minutes(amount: i32) -> Self {
Self {
amount,
unit: DurationUnit::Minute,
}
}
pub fn days(amount: i32) -> Self {
Self {
amount,
unit: DurationUnit::Day,
}
}
pub fn as_minutes(&self) -> i32 {
match self.unit {
DurationUnit::Minute => self.amount,
DurationUnit::Day => self.amount * 24 * 60,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DurationUnit {
Minute,
Day,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReminderType {
Relative,
Absolute,
Location,
}
impl std::fmt::Display for ReminderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReminderType::Relative => write!(f, "relative"),
ReminderType::Absolute => write!(f, "absolute"),
ReminderType::Location => write!(f, "location"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LocationTrigger {
OnEnter,
OnLeave,
}
impl std::fmt::Display for LocationTrigger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LocationTrigger::OnEnter => write!(f, "on_enter"),
LocationTrigger::OnLeave => write!(f, "on_leave"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Datelike;
#[test]
fn test_due_from_date() {
let due = Due::from_date("2026-01-25");
assert_eq!(due.date, "2026-01-25");
assert!(!due.is_recurring);
assert!(!due.has_time());
}
#[test]
fn test_due_from_datetime() {
let due = Due::from_datetime("2026-01-25", "2026-01-25T15:00:00Z");
assert_eq!(due.date, "2026-01-25");
assert_eq!(due.datetime, Some("2026-01-25T15:00:00Z".to_string()));
assert!(due.has_time());
}
#[test]
fn test_due_as_naive_date() {
let due = Due::from_date("2026-01-25");
let date = due.as_naive_date().unwrap();
assert_eq!(date.year(), 2026);
assert_eq!(date.month(), 1);
assert_eq!(date.day(), 25);
}
#[test]
fn test_due_deserialize() {
let json = r#"{
"date": "2026-01-25",
"datetime": "2026-01-25T15:00:00Z",
"is_recurring": false,
"string": "tomorrow at 3pm",
"timezone": "America/New_York"
}"#;
let due: Due = serde_json::from_str(json).unwrap();
assert_eq!(due.date, "2026-01-25");
assert!(due.has_time());
assert!(!due.is_recurring);
}
#[test]
fn test_deadline_deserialize() {
let json = r#"{"date": "2026-01-30"}"#;
let deadline: Deadline = serde_json::from_str(json).unwrap();
assert_eq!(deadline.date, "2026-01-30");
}
#[test]
fn test_duration_minutes() {
let duration = Duration::minutes(30);
assert_eq!(duration.amount, 30);
assert_eq!(duration.unit, DurationUnit::Minute);
assert_eq!(duration.as_minutes(), 30);
}
#[test]
fn test_duration_days() {
let duration = Duration::days(2);
assert_eq!(duration.amount, 2);
assert_eq!(duration.unit, DurationUnit::Day);
assert_eq!(duration.as_minutes(), 2 * 24 * 60);
}
#[test]
fn test_duration_unit_serialize() {
let minute = DurationUnit::Minute;
let day = DurationUnit::Day;
assert_eq!(serde_json::to_string(&minute).unwrap(), "\"minute\"");
assert_eq!(serde_json::to_string(&day).unwrap(), "\"day\"");
}
#[test]
fn test_duration_unit_deserialize() {
let minute: DurationUnit = serde_json::from_str("\"minute\"").unwrap();
let day: DurationUnit = serde_json::from_str("\"day\"").unwrap();
assert_eq!(minute, DurationUnit::Minute);
assert_eq!(day, DurationUnit::Day);
}
#[test]
fn test_duration_deserialize() {
let json = r#"{"amount": 15, "unit": "minute"}"#;
let duration: Duration = serde_json::from_str(json).unwrap();
assert_eq!(duration.amount, 15);
assert_eq!(duration.unit, DurationUnit::Minute);
}
#[test]
fn test_reminder_type_serialize() {
assert_eq!(
serde_json::to_string(&ReminderType::Relative).unwrap(),
"\"relative\""
);
assert_eq!(
serde_json::to_string(&ReminderType::Absolute).unwrap(),
"\"absolute\""
);
assert_eq!(
serde_json::to_string(&ReminderType::Location).unwrap(),
"\"location\""
);
}
#[test]
fn test_reminder_type_deserialize() {
let relative: ReminderType = serde_json::from_str("\"relative\"").unwrap();
let absolute: ReminderType = serde_json::from_str("\"absolute\"").unwrap();
let location: ReminderType = serde_json::from_str("\"location\"").unwrap();
assert_eq!(relative, ReminderType::Relative);
assert_eq!(absolute, ReminderType::Absolute);
assert_eq!(location, ReminderType::Location);
}
#[test]
fn test_reminder_type_display() {
assert_eq!(ReminderType::Relative.to_string(), "relative");
assert_eq!(ReminderType::Absolute.to_string(), "absolute");
assert_eq!(ReminderType::Location.to_string(), "location");
}
#[test]
fn test_location_trigger_serialize() {
assert_eq!(
serde_json::to_string(&LocationTrigger::OnEnter).unwrap(),
"\"on_enter\""
);
assert_eq!(
serde_json::to_string(&LocationTrigger::OnLeave).unwrap(),
"\"on_leave\""
);
}
#[test]
fn test_location_trigger_deserialize() {
let on_enter: LocationTrigger = serde_json::from_str("\"on_enter\"").unwrap();
let on_leave: LocationTrigger = serde_json::from_str("\"on_leave\"").unwrap();
assert_eq!(on_enter, LocationTrigger::OnEnter);
assert_eq!(on_leave, LocationTrigger::OnLeave);
}
#[test]
fn test_location_trigger_display() {
assert_eq!(LocationTrigger::OnEnter.to_string(), "on_enter");
assert_eq!(LocationTrigger::OnLeave.to_string(), "on_leave");
}
}