Skip to main content

jmap_tasks_client/methods/
task_notification.rs

1//! JMAP Tasks — TaskNotification/* method implementations on SessionClient.
2//!
3//! TaskNotification/set is destroy-only. Any create or update entries from
4//! the client would be rejected by the server with `forbidden`, but we
5//! expose a typed API that only allows destroy to avoid client-side confusion.
6
7use jmap_types::{Id, State};
8
9use super::{ChangesResponse, GetResponse, QueryChangesResponse, QueryResponse, SetResponse};
10
11impl super::SessionClient {
12    /// Fetch TaskNotification objects by IDs (draft-tasks-06 §5.2).
13    ///
14    /// If `ids` is `None`, the server returns all TaskNotifications for the account.
15    pub async fn task_notification_get(
16        &self,
17        ids: Option<&[Id]>,
18        properties: Option<&[&str]>,
19    ) -> Result<GetResponse<jmap_tasks_types::TaskNotification>, jmap_base_client::ClientError>
20    {
21        let (api_url, account_id) = self.session_parts()?;
22        // Omit `ids` / `properties` when None — see the matching comment on
23        // `task_list_get` for the rationale (consistent with set/changes/query).
24        let mut args = serde_json::json!({ "accountId": account_id });
25        if let Some(id_slice) = ids {
26            args["ids"] = serde_json::to_value(id_slice).expect("Id slice Serialize is infallible");
27        }
28        if let Some(props) = properties {
29            args["properties"] = serde_json::Value::Array(
30                props.iter().copied().map(serde_json::Value::from).collect(),
31            );
32        }
33        let req = super::build_request("TaskNotification/get", args, super::USING_TASKS);
34        let resp = self.call_internal(api_url, &req).await?;
35        jmap_base_client::extract_response(&resp, super::CALL_ID)
36    }
37
38    /// Fetch changes to TaskNotification objects since `since_state`
39    /// (draft-tasks-06 §5.3).
40    pub async fn task_notification_changes(
41        &self,
42        since_state: &State,
43        max_changes: Option<u64>,
44    ) -> Result<ChangesResponse, jmap_base_client::ClientError> {
45        // Defence-in-depth: see task_list_changes.
46        if since_state.as_ref().is_empty() {
47            return Err(jmap_base_client::ClientError::InvalidArgument(
48                "task_notification_changes: since_state may not be empty".into(),
49            ));
50        }
51        let (api_url, account_id) = self.session_parts()?;
52        let mut args = serde_json::json!({
53            "accountId": account_id,
54            "sinceState": since_state,
55        });
56        if let Some(mc) = max_changes {
57            args["maxChanges"] = mc.into();
58        }
59        let req = super::build_request("TaskNotification/changes", args, super::USING_TASKS);
60        let resp = self.call_internal(api_url, &req).await?;
61        jmap_base_client::extract_response(&resp, super::CALL_ID)
62    }
63
64    /// Destroy TaskNotification objects (draft-tasks-06 §5.4).
65    ///
66    /// **Destroy-only**: TaskNotification/set only supports `destroy`.
67    /// The server creates notifications automatically; clients may only remove them.
68    ///
69    /// Passing an empty `destroy` list is valid and produces an empty /set response.
70    pub async fn task_notification_set(
71        &self,
72        destroy: Vec<Id>,
73    ) -> Result<SetResponse, jmap_base_client::ClientError> {
74        let (api_url, account_id) = self.session_parts()?;
75        let args = serde_json::json!({
76            "accountId": account_id,
77            "destroy": destroy,
78        });
79        let req = super::build_request("TaskNotification/set", args, super::USING_TASKS);
80        let resp = self.call_internal(api_url, &req).await?;
81        jmap_base_client::extract_response(&resp, super::CALL_ID)
82    }
83
84    /// Query TaskNotification IDs with optional filter and sort
85    /// (draft-tasks-06 §5.5).
86    pub async fn task_notification_query(
87        &self,
88        filter: Option<serde_json::Value>,
89        sort: Option<serde_json::Value>,
90        position: Option<u64>,
91        limit: Option<u64>,
92    ) -> Result<QueryResponse, jmap_base_client::ClientError> {
93        let (api_url, account_id) = self.session_parts()?;
94        let mut args = serde_json::json!({
95            "accountId": account_id,
96        });
97        if let Some(f) = filter {
98            args["filter"] = f;
99        }
100        if let Some(s) = sort {
101            args["sort"] = s;
102        }
103        if let Some(p) = position {
104            args["position"] = p.into();
105        }
106        if let Some(l) = limit {
107            args["limit"] = l.into();
108        }
109        let req = super::build_request("TaskNotification/query", args, super::USING_TASKS);
110        let resp = self.call_internal(api_url, &req).await?;
111        jmap_base_client::extract_response(&resp, super::CALL_ID)
112    }
113
114    /// Fetch query-result changes for TaskNotification since `since_query_state`
115    /// (draft-tasks-06 §5.6).
116    pub async fn task_notification_query_changes(
117        &self,
118        since_query_state: &State,
119        max_changes: Option<u64>,
120    ) -> Result<QueryChangesResponse, jmap_base_client::ClientError> {
121        // Defence-in-depth: see task_list_changes.
122        if since_query_state.as_ref().is_empty() {
123            return Err(jmap_base_client::ClientError::InvalidArgument(
124                "task_notification_query_changes: since_query_state may not be empty".into(),
125            ));
126        }
127        let (api_url, account_id) = self.session_parts()?;
128        let mut args = serde_json::json!({
129            "accountId": account_id,
130            "sinceQueryState": since_query_state,
131        });
132        if let Some(mc) = max_changes {
133            args["maxChanges"] = mc.into();
134        }
135        let req = super::build_request("TaskNotification/queryChanges", args, super::USING_TASKS);
136        let resp = self.call_internal(api_url, &req).await?;
137        jmap_base_client::extract_response(&resp, super::CALL_ID)
138    }
139}
140
141// ---------------------------------------------------------------------------
142// Tests — see tests/task_notification_tests.rs (wiremock-backed end-to-end)
143// ---------------------------------------------------------------------------
144//
145// `task_notification_set_destroy_only_serialization` and
146// `task_notification_set_uses_tasks_capability` were vacuous: they
147// hand-built `args` Values and fed them to `build_request`, never
148// exercising the production `task_notification_set` builder. Deleted in
149// JMAP-tco1.20.
150//
151// Real production-path coverage:
152//   - task_notification_get_round_trip
153//   - task_notification_changes_round_trip
154//   - task_notification_set_destroy_only_wire_format
155//   - task_notification_set_empty_destroy_succeeds
156//   - task_notification_query_with_filter
157//   - task_notification_query_changes_round_trip
158// in tests/task_notification_tests.rs.
159//
160// Specific-flag passthrough coverage that may be lost is tracked
161// under JMAP-uuoi for follow-up wiremock smoke tests.
162//
163// `build_request`, `CALL_ID`, and `USING_TASKS` themselves have their
164// own focused tests in `methods/mod.rs`.
165//
166// The InvalidArgument guard for empty since_state lives in
167// task_notification_changes production code; testing it requires a
168// wiremock-backed async harness. See JMAP-sc1b.64.