use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
use crate::{Comment, NativeId, Priority, StatusCategory};
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct TaskQuery {
pub text: Option<TextQuery>,
pub labels: LabelFilter,
pub statuses: Vec<StatusCategory>,
pub project: ProjectFilter,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[schemars(!skip_serializing_if)]
pub priorities: Vec<Priority>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commented_since: Option<DateTime<Utc>>,
}
impl TaskQuery {
#[must_use]
pub fn comments_match<'a>(&self, comments: impl IntoIterator<Item = &'a Comment>) -> bool {
let Some(since) = self.commented_since else {
return true;
};
comments
.into_iter()
.any(|comment| commented_at_or_after(comment, since))
}
}
fn commented_at_or_after(comment: &Comment, since: DateTime<Utc>) -> bool {
comment.created_at.is_some_and(|at| at >= since)
|| comment.updated_at.is_some_and(|at| at >= since)
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct ProjectQuery {
pub text: Option<TextQuery>,
pub labels: LabelFilter,
pub statuses: Vec<StatusCategory>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct DocumentQuery {
pub text: Option<TextQuery>,
pub labels: LabelFilter,
pub project: ProjectFilter,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TextQuery {
pub terms: String,
pub fields: TextFields,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TextFields {
Title,
Content,
TitleOrContent,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct LabelFilter {
pub any_of: Vec<String>,
pub all_of: Vec<String>,
pub none_of: Vec<String>,
}
impl LabelFilter {
#[must_use]
pub fn is_empty(&self) -> bool {
self.any_of.is_empty() && self.all_of.is_empty() && self.none_of.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum ProjectFilter {
#[default]
Any,
Orphans,
Is(NativeId),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct PageRequest {
pub cursor: Option<Cursor>,
#[serde(deserialize_with = "non_zero_limit")]
pub limit: u32,
}
fn non_zero_limit<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u32, D::Error> {
let value = u32::deserialize(deserializer)?;
if value == 0 {
return Err(D::Error::custom(
"limit must be at least 1; a page of no rows is not a page",
));
}
Ok(value)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Page<T> {
pub items: Vec<T>,
pub next: Option<Cursor>,
}
impl<T> Page<T> {
#[must_use]
pub fn last(items: Vec<T>) -> Self {
Self { items, next: None }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct Cursor(pub String);