use chrono::NaiveDate;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
#[cfg(feature = "serde")]
use serde::ser::SerializeStruct;
#[cfg(feature = "serde")]
use serde::{Serialize, Serializer};
use crate::parser::{self, Input};
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Description<'a>(pub(crate) Located<Cow<'a, str>>);
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Priority(pub(crate) Located<char>);
#[cfg_attr(
feature = "serde",
derive(Serialize),
serde(tag = "type", rename_all = "lowercase")
)]
#[derive(Clone, Debug, PartialEq)]
pub enum Tag<'a> {
Context(Located<&'a str>),
Project(Located<&'a str>),
Named(Located<(&'a str, &'a str)>),
}
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Located<T> {
pub(crate) data: T,
pub(crate) span: Span,
}
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Span {
start: usize,
end: usize,
}
#[derive(Clone)]
#[non_exhaustive]
pub struct Todo<'a> {
pub line: u32,
pub x: Option<Span>,
pub priority: Option<Priority>,
pub date_completed: Option<Located<NaiveDate>>,
pub date_started: Option<Located<NaiveDate>>,
pub description: Description<'a>,
}
impl Description<'_> {
pub fn text(&self) -> &str {
self.0.data.as_ref()
}
pub fn span(&self) -> &Span {
self.0.span()
}
}
impl Description<'_> {
pub fn into_string(self) -> String {
self.0.data.into_owned()
}
}
impl<T> Located<T> {
pub fn data(&self) -> &T {
&self.data
}
pub fn span(&self) -> &Span {
&self.span
}
}
impl Priority {
pub fn rank(&self) -> char {
self.0.data
}
pub fn span(&self) -> &Span {
self.0.span()
}
}
impl PartialOrd for Priority {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.rank()
.partial_cmp(&other.rank())
.map(Ordering::reverse)
}
}
impl Span {
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
}
impl Span {
pub(crate) fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub(crate) fn locate(input: &Input, len: usize) -> Self {
let start = input.get_utf8_column() - 1;
let end = start + len;
Self { start, end }
}
}
impl Todo<'_> {
pub fn tags(&self) -> impl Iterator<Item = Tag> {
parser::tags(&self.description)
}
pub fn is_done(&self) -> bool {
matches!(
self,
Self { x: Some(_), .. }
| Self {
date_completed: Some(_),
..
}
)
}
pub fn into_owned(self) -> Todo<'static> {
let Description(Located { data, span }) = self.description;
Todo {
description: Description(Located {
data: Cow::Owned(data.into_owned()),
span,
}),
..self
}
}
}
impl Debug for Todo<'_> {
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
struct DebugTags<'a> {
todo: &'a Todo<'a>,
}
impl Debug for DebugTags<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_list().entries(self.todo.tags()).finish()
}
}
fmt.debug_struct("Todo")
.field("line", &self.line)
.field("x", &self.x)
.field("priority", &self.priority)
.field("date_completed", &self.date_completed)
.field("date_started", &self.date_started)
.field("description", &self.description)
.field("tags", &DebugTags { todo: self })
.finish()
}
}
#[cfg(feature = "serde")]
impl Serialize for Todo<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
struct SerializeTags<'a> {
todo: &'a Todo<'a>,
}
impl Serialize for SerializeTags<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(self.todo.tags())
}
}
let mut state = serializer.serialize_struct("Todo", 1)?;
state.serialize_field("x", &self.x)?;
state.serialize_field("priority", &self.priority)?;
state.serialize_field("date_completed", &self.date_completed)?;
state.serialize_field("date_started", &self.date_started)?;
state.serialize_field("description", &self.description)?;
state.serialize_field("tags", &SerializeTags { todo: &self })?;
state.end()
}
}
#[cfg(test)]
mod tests {
use super::{Located, Priority, Span};
#[test]
fn test_priority_order() {
let a = Priority(Located {
data: 'A',
span: Span::new(0, 0),
});
let b = Priority(Located {
data: 'B',
span: Span::new(0, 0),
});
assert!(
a > b,
"Priority should be ordered alphabetically in descending order."
)
}
}