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, Priority, 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    /// Priorities to keep: a task matches when its priority is any one of these. Empty means
23    /// unfiltered.
24    ///
25    /// Defaulted when absent and left out of the wire when empty, so a plugin written before
26    /// there were priorities reads exactly the query it read before — and, declaring no
27    /// [`Capabilities::filter_by_priority`](crate::Capabilities::filter_by_priority), is never
28    /// handed one it would have to ignore.
29    #[serde(default, skip_serializing_if = "Vec::is_empty")]
30    #[schemars(!skip_serializing_if)]
31    pub priorities: Vec<Priority>,
32}
33
34/// A filter over a source's projects.
35#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
36pub struct ProjectQuery {
37    /// Free-text search, when the caller asked for one.
38    pub text: Option<TextQuery>,
39    /// Label membership.
40    pub labels: LabelFilter,
41    /// Status categories to keep. Empty means unfiltered.
42    pub statuses: Vec<StatusCategory>,
43}
44
45/// A filter over a source's documents.
46///
47/// No statuses, deliberately: a [`Document`](crate::Document) is not work and carries no
48/// status, so there is nothing here for a status filter to compare against.
49#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
50pub struct DocumentQuery {
51    /// Free-text search, when the caller asked for one.
52    pub text: Option<TextQuery>,
53    /// Label membership.
54    pub labels: LabelFilter,
55    /// Which project the document lives in.
56    pub project: ProjectFilter,
57}
58
59/// A free-text search and the fields it searches.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
61pub struct TextQuery {
62    /// What the user typed.
63    pub terms: String,
64    /// Where to look for it.
65    pub fields: TextFields,
66}
67
68/// Which fields a [`TextQuery`] searches.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
70#[serde(rename_all = "kebab-case")]
71pub enum TextFields {
72    /// Titles only.
73    Title,
74    /// Bodies only.
75    Content,
76    /// Either one matching is a match.
77    TitleOrContent,
78}
79
80/// Label membership, by **name** rather than by id.
81///
82/// A label id is per-source; a user filtering across sources types a word. Names
83/// are matched case-insensitively for the same reason.
84#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
85pub struct LabelFilter {
86    /// Keep an item carrying at least one of these.
87    pub any_of: Vec<String>,
88    /// Keep an item carrying all of these.
89    pub all_of: Vec<String>,
90    /// Drop an item carrying any of these.
91    pub none_of: Vec<String>,
92}
93
94impl LabelFilter {
95    /// Whether this filter constrains anything at all.
96    #[must_use]
97    pub fn is_empty(&self) -> bool {
98        self.any_of.is_empty() && self.all_of.is_empty() && self.none_of.is_empty()
99    }
100}
101
102/// Which project a task must belong to.
103#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
104#[serde(rename_all = "kebab-case")]
105pub enum ProjectFilter {
106    /// No constraint.
107    #[default]
108    Any,
109    /// Only tasks belonging to no project.
110    Orphans,
111    /// Only tasks belonging to this project.
112    Is(NativeId),
113}
114
115/// One step of a walk through a result set.
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
117pub struct PageRequest {
118    /// Where to resume, or `None` to start at the beginning.
119    pub cursor: Option<Cursor>,
120    /// The most items to return, at least 1. A source may return fewer, never more.
121    #[serde(deserialize_with = "non_zero_limit")]
122    // 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.
123    pub limit: u32,
124}
125
126/// Reject a zero page size where a request is read, so an ask for no rows never reaches a
127/// source as if it were an ask for one.
128fn non_zero_limit<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u32, D::Error> {
129    let value = u32::deserialize(deserializer)?;
130    if value == 0 {
131        return Err(D::Error::custom(
132            "limit must be at least 1; a page of no rows is not a page",
133        ));
134    }
135    Ok(value)
136}
137
138/// One page of results, and where to pick up.
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
140pub struct Page<T> {
141    /// This page's items, in the source's stable order.
142    pub items: Vec<T>,
143    /// The cursor for the next page, or `None` when the walk is exhausted.
144    pub next: Option<Cursor>,
145}
146
147impl<T> Page<T> {
148    /// The last page of a walk: these items and nothing after them.
149    #[must_use]
150    pub fn last(items: Vec<T>) -> Self {
151        Self { items, next: None }
152    }
153}
154
155/// A plugin-defined resume token. The engine stores and returns one; it never
156/// interprets one.
157#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
158#[serde(transparent)]
159pub struct Cursor(pub String);