Skip to main content

caldir_core/
remote.rs

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
13/// provider with config should resolve to a unique remote
14pub 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        // Local copy was written by an old parser that dropped params;
191        // remote still carries them. Push must send the union so we don't
192        // strip provider-managed metadata.
193        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}