Skip to main content

onetaskgraph_plugin_api/
query.rs

1//! What a caller asks a source for, and how a source hands back more than fits
2//! in one answer.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
6
7use crate::{NativeId, StatusCategory};
8
9/// A filter over a source's tasks.
10///
11/// Every field narrows; an empty or `None` field means unfiltered.
12#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
13pub struct TaskQuery {
14    /// Free-text search, when the caller asked for one.
15    pub text: Option<TextQuery>,
16    /// Label membership.
17    pub labels: LabelFilter,
18    /// Status categories to keep. Empty means unfiltered.
19    pub statuses: Vec<StatusCategory>,
20    /// Which project the task belongs to.
21    pub project: ProjectFilter,
22}
23
24/// A filter over a source's projects.
25#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
26pub struct ProjectQuery {
27    /// Free-text search, when the caller asked for one.
28    pub text: Option<TextQuery>,
29    /// Label membership.
30    pub labels: LabelFilter,
31    /// Status categories to keep. Empty means unfiltered.
32    pub statuses: Vec<StatusCategory>,
33}
34
35/// A free-text search and the fields it searches.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
37pub struct TextQuery {
38    /// What the user typed.
39    pub terms: String,
40    /// Where to look for it.
41    pub fields: TextFields,
42}
43
44/// Which fields a [`TextQuery`] searches.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
46#[serde(rename_all = "kebab-case")]
47pub enum TextFields {
48    /// Titles only.
49    Title,
50    /// Bodies only.
51    Content,
52    /// Either one matching is a match.
53    TitleOrContent,
54}
55
56/// Label membership, by **name** rather than by id.
57///
58/// A label id is per-source; a user filtering across sources types a word. Names
59/// are matched case-insensitively for the same reason.
60#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
61pub struct LabelFilter {
62    /// Keep an item carrying at least one of these.
63    pub any_of: Vec<String>,
64    /// Keep an item carrying all of these.
65    pub all_of: Vec<String>,
66    /// Drop an item carrying any of these.
67    pub none_of: Vec<String>,
68}
69
70impl LabelFilter {
71    /// Whether this filter constrains anything at all.
72    #[must_use]
73    pub fn is_empty(&self) -> bool {
74        self.any_of.is_empty() && self.all_of.is_empty() && self.none_of.is_empty()
75    }
76}
77
78/// Which project a task must belong to.
79#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
80#[serde(rename_all = "kebab-case")]
81pub enum ProjectFilter {
82    /// No constraint.
83    #[default]
84    Any,
85    /// Only tasks belonging to no project.
86    Orphans,
87    /// Only tasks belonging to this project.
88    Is(NativeId),
89}
90
91/// One step of a walk through a result set.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
93pub struct PageRequest {
94    /// Where to resume, or `None` to start at the beginning.
95    pub cursor: Option<Cursor>,
96    /// The most items to return, at least 1. A source may return fewer, never more.
97    #[serde(deserialize_with = "non_zero_limit")]
98    // llmlint: ignore[invalid_states_unrepresentable] this field's wire shape is frozen by the plugin contract every source is written against; only the contract's owner may change it, and tightening it is post-build follow-up.
99    pub limit: u32,
100}
101
102/// Reject a zero page size where a request is read, so an ask for no rows never reaches a
103/// source as if it were an ask for one.
104fn non_zero_limit<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u32, D::Error> {
105    let value = u32::deserialize(deserializer)?;
106    if value == 0 {
107        return Err(D::Error::custom(
108            "limit must be at least 1; a page of no rows is not a page",
109        ));
110    }
111    Ok(value)
112}
113
114/// One page of results, and where to pick up.
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
116pub struct Page<T> {
117    /// This page's items, in the source's stable order.
118    pub items: Vec<T>,
119    /// The cursor for the next page, or `None` when the walk is exhausted.
120    pub next: Option<Cursor>,
121}
122
123impl<T> Page<T> {
124    /// The last page of a walk: these items and nothing after them.
125    #[must_use]
126    pub fn last(items: Vec<T>) -> Self {
127        Self { items, next: None }
128    }
129}
130
131/// A plugin-defined resume token. The engine stores and returns one; it never
132/// interprets one.
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
134#[serde(transparent)]
135pub struct Cursor(pub String);