use std::convert::TryFrom;
use std::str::FromStr as _;
use anyhow::Error;
use anyhow::Result;
use icalendar::Calendar;
use icalendar::Component as _;
use icalendar::Todo;
use crate::ser::tags::Tag;
use crate::ser::tasks::Id as TaskId;
use crate::ser::tasks::Task;
use crate::LINE_END;
use crate::LINE_END_STR;
use super::util::emit_list;
use super::util::parse_list;
use super::util::try_from_calendar_with_single_todo;
use super::SerICal;
const TAGS_PROPERTY: &str = "TAGS";
const POSITION_PROPERTY: &str = "POSITION";
impl From<&Task> for Todo {
fn from(task: &Task) -> Self {
let mut todo = Todo::new();
todo.uid(&task.id.as_hyphenated().to_string());
todo.summary(&task.summary);
if !task.details.is_empty() {
todo.description(&task.details.replace(LINE_END, "\n"));
}
if let Some(tags) = emit_list(&task.tags) {
todo.add_property(TAGS_PROPERTY, &tags);
}
if let Some(position) = &task.position {
todo.add_property(POSITION_PROPERTY, position.to_string());
}
todo
}
}
impl From<&Task> for Calendar {
fn from(task: &Task) -> Self {
let todo = Todo::from(task);
let calendar = Calendar::from([todo]);
calendar
}
}
impl TryFrom<&Todo> for Task {
type Error = Error;
fn try_from(todo: &Todo) -> Result<Self, Self::Error> {
let id = todo
.get_uid()
.map(TaskId::from_str)
.transpose()?
.unwrap_or_else(TaskId::new_v4);
let summary = todo.get_summary().unwrap_or("").to_string();
let details = todo
.get_description()
.unwrap_or("")
.to_string()
.replace('\n', LINE_END_STR);
let tags = todo
.property_value(TAGS_PROPERTY)
.map(parse_list::<Tag>)
.unwrap_or_else(|| Ok(Vec::new()))?;
let position = todo
.property_value(POSITION_PROPERTY)
.map(f64::from_str)
.transpose()?;
Ok(Task {
id,
summary,
details,
tags,
position,
})
}
}
impl TryFrom<&Calendar> for Task {
type Error = Error;
fn try_from(calendar: &Calendar) -> Result<Self, Self::Error> {
try_from_calendar_with_single_todo::<Self>(calendar)
}
}
impl SerICal for Task {
#[inline]
fn to_ical_string(&self) -> String {
let calendar = Calendar::from(self);
calendar.to_string()
}
#[inline]
fn from_ical_string(data: &str) -> Result<Self, Error> {
let calendar = Calendar::from_str(data).map_err(Error::msg)?;
let task = Task::try_from(&calendar)?;
Ok(task)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ser::tags::Id as TagId;
use crate::LINE_END;
use super::super::iCal;
use super::super::Backend;
#[test]
fn serialize_deserialize_task() {
let task = Task::new("test task");
let data = iCal::serialize(&task).unwrap();
let new_task = <iCal as Backend<Task>>::deserialize(&data).unwrap();
assert_eq!(new_task, task);
}
#[test]
fn serialize_deserialize_task_with_tag() {
let tags = [Tag::from(TagId::try_from(1337).unwrap())];
let task = Task::new("test task").with_tags(tags);
let data = iCal::serialize(&task).unwrap();
let new_task = <iCal as Backend<Task>>::deserialize(&data).unwrap();
assert_eq!(new_task, task);
}
#[test]
fn serialize_deserialize_task_with_multiline_details() {
let details = format!("multi-{LINE_END}line{LINE_END}string");
let task = Task::new("test task").with_details(details);
let data = iCal::serialize(&task).unwrap();
let new_task = <iCal as Backend<Task>>::deserialize(&data).unwrap();
assert_eq!(new_task, task);
}
}