use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
Pending,
Waiting,
Completed,
Deleted,
Recurring,
}
impl TaskStatus {
pub const fn should_sync_to_habitica(&self) -> bool {
matches!(
self,
TaskStatus::Pending | TaskStatus::Waiting | TaskStatus::Completed
)
}
pub fn is_completed(&self) -> bool {
*self == TaskStatus::Completed
}
pub const fn is_pending(&self) -> bool {
matches!(self, TaskStatus::Pending | TaskStatus::Waiting)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TaskDifficulty {
Trivial,
#[default]
Easy,
Medium,
Hard,
}
impl TaskDifficulty {
pub const fn to_habitica_priority(&self) -> f64 {
match self {
TaskDifficulty::Trivial => 0.1,
TaskDifficulty::Easy => 1.0,
TaskDifficulty::Medium => 1.5,
TaskDifficulty::Hard => 2.0,
}
}
pub fn from_habitica_priority(priority: f64) -> Self {
if (priority - 0.1).abs() < 0.01 {
TaskDifficulty::Trivial
} else if (priority - 1.0).abs() < 0.01 {
TaskDifficulty::Easy
} else if (priority - 1.5).abs() < 0.01 {
TaskDifficulty::Medium
} else {
TaskDifficulty::Hard
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TaskType {
#[default]
Todo,
Daily,
#[serde(rename = "habit")]
Habit,
#[serde(rename = "reward")]
Reward,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Annotation {
pub entry: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub uuid: Uuid,
pub description: String,
pub status: TaskStatus,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "super::date_format::deserialize_opt",
default
)]
pub modified: Option<DateTime<Utc>>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "super::date_format::deserialize_opt",
default
)]
pub due: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<Vec<Annotation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub habitica_uuid: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub habitica_difficulty: Option<TaskDifficulty>,
#[serde(skip_serializing_if = "Option::is_none")]
pub habitica_task_type: Option<TaskType>,
#[serde(flatten)]
pub extra: serde_json::Map<String, Value>,
}
impl Task {
pub fn modified_or_now(&self) -> DateTime<Utc> {
self.modified.unwrap_or_else(Utc::now)
}
pub fn difficulty(&self) -> TaskDifficulty {
self.habitica_difficulty.unwrap_or_default()
}
pub fn task_type(&self) -> TaskType {
self.habitica_task_type.unwrap_or_default()
}
pub fn has_note_annotation(&self, note_prefix: &str) -> bool {
self.annotations.as_ref().is_some_and(|annos| {
annos
.iter()
.any(|anno| anno.description.trim().starts_with(note_prefix))
})
}
pub fn filter_note_annotations(&self, note_prefix: &str) -> Vec<Annotation> {
self.annotations
.as_ref()
.map(|annos| {
annos
.iter()
.filter(|anno| !anno.description.trim().starts_with(note_prefix))
.cloned()
.collect()
})
.unwrap_or_default()
}
}
impl PartialEq for Task {
fn eq(&self, other: &Self) -> bool {
self.uuid == other.uuid
&& self.description == other.description
&& self.status == other.status
&& self.due == other.due
&& self.habitica_uuid == other.habitica_uuid
&& self.habitica_difficulty == other.habitica_difficulty
&& self.habitica_task_type == other.habitica_task_type
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_difficulty_conversion() {
assert_eq!(TaskDifficulty::Trivial.to_habitica_priority(), 0.1);
assert_eq!(TaskDifficulty::Easy.to_habitica_priority(), 1.0);
assert_eq!(TaskDifficulty::Medium.to_habitica_priority(), 1.5);
assert_eq!(TaskDifficulty::Hard.to_habitica_priority(), 2.0);
assert_eq!(
TaskDifficulty::from_habitica_priority(0.1),
TaskDifficulty::Trivial
);
assert_eq!(
TaskDifficulty::from_habitica_priority(1.0),
TaskDifficulty::Easy
);
assert_eq!(
TaskDifficulty::from_habitica_priority(1.5),
TaskDifficulty::Medium
);
assert_eq!(
TaskDifficulty::from_habitica_priority(2.0),
TaskDifficulty::Hard
);
}
#[test]
fn test_task_status_sync() {
assert!(TaskStatus::Pending.should_sync_to_habitica());
assert!(TaskStatus::Waiting.should_sync_to_habitica());
assert!(TaskStatus::Completed.should_sync_to_habitica());
assert!(!TaskStatus::Deleted.should_sync_to_habitica());
assert!(!TaskStatus::Recurring.should_sync_to_habitica());
}
}