1mod config;
2mod error;
3mod event;
4
5use crate::diff::EventChange;
6use crate::provider::ProviderError;
7use crate::{DateRange, Event, Provider, rpc};
8
9pub use config::{RemoteConfig, RemoteConfigParams};
10pub use error::RemoteError;
11pub use event::RemoteEvent;
12
13pub struct Remote {
15 provider: Provider,
16 params: RemoteConfigParams,
17}
18
19impl Remote {
20 pub fn new(provider: Provider, params: RemoteConfigParams) -> Self {
21 Self { provider, params }
22 }
23
24 pub async fn list_events(&self, range: &DateRange) -> Result<Vec<RemoteEvent>, RemoteError> {
25 let (from, to) = range.to_rfc3339();
26 let events = self
27 .provider
28 .call(rpc::ListEvents {
29 remote: self.params.clone(),
30 from,
31 to,
32 })
33 .await?
34 .into_iter()
35 .map(RemoteEvent::new)
36 .collect();
37
38 Ok(events)
39 }
40
41 pub async fn apply_change(
42 &self,
43 change: &EventChange,
44 ) -> Result<Option<RemoteEvent>, RemoteError> {
45 match change {
46 EventChange::Create(event) => {
47 let remote_event = self.create_event(event.clone()).await?;
48 Ok(Some(remote_event))
49 }
50 EventChange::Update { from, to } => {
51 let merged = to.clone().with_x_properties_merged_from(from);
52 let remote_event = self.update_event(merged).await?;
53 Ok(Some(remote_event))
54 }
55 EventChange::Delete(event) => {
56 self.delete_event(event.clone()).await?;
57 Ok(None)
58 }
59 }
60 }
61
62 async fn create_event(&self, event: Event) -> Result<RemoteEvent, RemoteError> {
63 let id = event.event_instance_id().to_string();
64 let event = self
65 .provider
66 .call(rpc::CreateEvent {
67 remote: self.params.clone(),
68 event,
69 })
70 .await
71 .map_err(|err| RemoteError::CreateEvent(id, err))?;
72
73 Ok(RemoteEvent::new(event))
74 }
75
76 async fn delete_event(&self, event: Event) -> Result<(), RemoteError> {
77 let id = event.event_instance_id().to_string();
78 self.provider
79 .call(rpc::DeleteEvent {
80 remote: self.params.clone(),
81 event,
82 })
83 .await
84 .map_err(|err| RemoteError::DeleteEvent(id, err))?;
85
86 Ok(())
87 }
88
89 async fn update_event(&self, event: Event) -> Result<RemoteEvent, RemoteError> {
90 let id = event.event_instance_id().to_string();
91 let event = self
92 .provider
93 .call(rpc::UpdateEvent {
94 remote: self.params.clone(),
95 event,
96 })
97 .await
98 .map_err(|err| RemoteError::UpdateEvent(id, err))?;
99
100 Ok(RemoteEvent::new(event))
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::test_utils::{test_event, test_remote};
108 use pretty_assertions::assert_eq;
109
110 #[tokio::test]
111 async fn failed_changes_identify_the_occurrence_and_preserve_provider_cause() {
112 use crate::provider::transport::mock_transport::MockTransport;
113 use std::sync::Arc;
114
115 let mut event = test_event();
116 event.recurrence_id = Some("20260108T120000Z".parse().unwrap());
117 let id = event.event_instance_id().to_string();
118 let message = " API rejected request: original detail\n";
119 for (operation, change) in [
120 ("create", EventChange::Create(event.clone())),
121 (
122 "update",
123 EventChange::Update {
124 from: event.clone(),
125 to: event.clone(),
126 },
127 ),
128 ("delete", EventChange::Delete(event.clone())),
129 ] {
130 let transport = Arc::new(MockTransport::with_response(rpc::Response::error(message)));
131 let provider = Provider::with_transport("test".into(), transport);
132 let remote = Remote::new(provider, RemoteConfigParams::new());
133 let error = remote
134 .apply_change(&change)
135 .await
136 .err()
137 .expect("provider failure");
138 let reported_id = match &error {
139 RemoteError::CreateEvent(id, _)
140 | RemoteError::UpdateEvent(id, _)
141 | RemoteError::DeleteEvent(id, _) => id,
142 other => panic!("unexpected error: {other:?}"),
143 };
144 assert_eq!(reported_id, &id);
145 let error = anyhow::Error::new(error);
146 assert!(matches!(error.root_cause().downcast_ref::<ProviderError>(),
147 Some(ProviderError::Provider(text)) if text == message));
148 assert_eq!(
149 format!("{error:#}"),
150 format!("failed to {operation} remote event {id}: {message}")
151 );
152 }
153 }
154
155 #[tokio::test]
156 async fn apply_change_sends_create_event_for_outgoing_create() {
157 let (mock, remote) = test_remote();
158 let event = test_event();
159 mock.reply::<rpc::CreateEvent>(event.clone());
160
161 remote
162 .apply_change(&EventChange::Create(event.clone()))
163 .await
164 .unwrap();
165
166 assert_eq!(mock.captured_request::<rpc::CreateEvent>().event, event);
167 }
168
169 #[tokio::test]
170 async fn apply_change_sends_update_event_for_outgoing_update() {
171 let (mock, remote) = test_remote();
172 let from = test_event();
173 let mut to = from.clone();
174 to.summary = Some("Updated".into());
175 mock.reply::<rpc::UpdateEvent>(to.clone());
176
177 remote
178 .apply_change(&EventChange::Update {
179 from,
180 to: to.clone(),
181 })
182 .await
183 .unwrap();
184
185 assert_eq!(mock.captured_request::<rpc::UpdateEvent>().event, to);
186 }
187
188 #[tokio::test]
189 async fn apply_change_update_merges_x_property_params_from_remote() {
190 let (mock, remote) = test_remote();
194
195 let mut from = test_event();
196 from.x_properties = vec![crate::event::XProperty {
197 name: "X-APPLE-STRUCTURED-LOCATION".to_string(),
198 value: "geo:51.47,-0.45".to_string(),
199 params: vec![("X-TITLE".to_string(), "London Heathrow".to_string())],
200 }];
201
202 let mut to = from.clone();
203 to.summary = Some("Updated".into());
204 to.x_properties = vec![crate::event::XProperty::new(
205 "X-APPLE-STRUCTURED-LOCATION",
206 "geo:51.47,-0.45",
207 )];
208
209 mock.reply::<rpc::UpdateEvent>(to.clone());
210
211 remote
212 .apply_change(&EventChange::Update { from, to })
213 .await
214 .unwrap();
215
216 let captured = mock.captured_request::<rpc::UpdateEvent>().event;
217 assert_eq!(captured.x_properties.len(), 1);
218 assert_eq!(
219 captured.x_properties[0].params,
220 vec![("X-TITLE".to_string(), "London Heathrow".to_string())]
221 );
222 }
223
224 #[tokio::test]
225 async fn apply_change_sends_delete_event_for_outgoing_delete() {
226 let (mock, remote) = test_remote();
227 let event = test_event();
228 mock.reply::<rpc::DeleteEvent>(());
229
230 remote
231 .apply_change(&EventChange::Delete(event.clone()))
232 .await
233 .unwrap();
234
235 assert_eq!(mock.captured_request::<rpc::DeleteEvent>().event, event);
236 }
237}