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);