tenseleyflow/rcal / e7111f1

Browse files

Prepare provider targets for Google Calendar

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
e7111f1a6a9b8784842cfd905b203bc50bdec7d8
Parents
efc8e07
Tree
d375879

2 changed files

StatusFile+-
M src/agenda.rs 249 68
M src/providers.rs 9 19
src/agenda.rsmodified
@@ -147,25 +147,32 @@ impl EventWriteTarget {
147147
         }
148148
     }
149149
 
150
-    pub fn microsoft(
150
+    pub fn provider(
151
+        provider_id: impl Into<String>,
151152
         account_id: impl Into<String>,
152153
         calendar_id: impl Into<String>,
153154
         label: impl Into<String>,
154155
     ) -> Self {
155156
         Self {
156
-            id: EventWriteTargetId::Microsoft {
157
-                account_id: account_id.into(),
158
-                calendar_id: calendar_id.into(),
159
-            },
157
+            id: EventWriteTargetId::provider(provider_id, account_id, calendar_id),
160158
             label: label.into(),
161159
         }
162160
     }
161
+
162
+    pub fn microsoft(
163
+        account_id: impl Into<String>,
164
+        calendar_id: impl Into<String>,
165
+        label: impl Into<String>,
166
+    ) -> Self {
167
+        Self::provider("microsoft", account_id, calendar_id, label)
168
+    }
163169
 }
164170
 
165171
 #[derive(Debug, Clone, PartialEq, Eq)]
166172
 pub enum EventWriteTargetId {
167173
     Local,
168
-    Microsoft {
174
+    Provider {
175
+        provider_id: String,
169176
         account_id: String,
170177
         calendar_id: String,
171178
     },
@@ -176,18 +183,84 @@ impl EventWriteTargetId {
176183
         matches!(self, Self::Local)
177184
     }
178185
 
186
+    pub fn provider(
187
+        provider_id: impl Into<String>,
188
+        account_id: impl Into<String>,
189
+        calendar_id: impl Into<String>,
190
+    ) -> Self {
191
+        Self::Provider {
192
+            provider_id: provider_id.into(),
193
+            account_id: account_id.into(),
194
+            calendar_id: calendar_id.into(),
195
+        }
196
+    }
197
+
198
+    pub fn microsoft(account_id: impl Into<String>, calendar_id: impl Into<String>) -> Self {
199
+        Self::provider("microsoft", account_id, calendar_id)
200
+    }
201
+
202
+    pub fn provider_id(&self) -> Option<&str> {
203
+        match self {
204
+            Self::Local => None,
205
+            Self::Provider { provider_id, .. } => Some(provider_id),
206
+        }
207
+    }
208
+
209
+    pub fn account_id(&self) -> Option<&str> {
210
+        match self {
211
+            Self::Local => None,
212
+            Self::Provider { account_id, .. } => Some(account_id),
213
+        }
214
+    }
215
+
216
+    pub fn calendar_id(&self) -> Option<&str> {
217
+        match self {
218
+            Self::Local => None,
219
+            Self::Provider { calendar_id, .. } => Some(calendar_id),
220
+        }
221
+    }
222
+
223
+    pub fn provider_parts(&self) -> Option<(&str, &str, &str)> {
224
+        match self {
225
+            Self::Local => None,
226
+            Self::Provider {
227
+                provider_id,
228
+                account_id,
229
+                calendar_id,
230
+            } => Some((provider_id, account_id, calendar_id)),
231
+        }
232
+    }
233
+
234
+    pub fn is_provider(&self, provider: &str) -> bool {
235
+        self.provider_id() == Some(provider)
236
+    }
237
+
238
+    pub fn is_microsoft(&self) -> bool {
239
+        self.is_provider("microsoft")
240
+    }
241
+
242
+    pub fn microsoft_parts(&self) -> Option<(&str, &str)> {
243
+        let (provider, account, calendar) = self.provider_parts()?;
244
+        (provider == "microsoft").then_some((account, calendar))
245
+    }
246
+
247
+    pub fn source_id(&self) -> Option<String> {
248
+        self.provider_parts()
249
+            .map(|(provider, account, calendar)| format!("{provider}:{account}:{calendar}"))
250
+    }
251
+
179252
     pub fn from_event(event: &Event) -> Option<Self> {
180253
         if event.is_local() {
181254
             return Some(Self::Local);
182255
         }
183256
         let mut parts = event.source.source_id.splitn(3, ':');
184
-        match (parts.next(), parts.next(), parts.next()) {
185
-            (Some("microsoft"), Some(account_id), Some(calendar_id)) => Some(Self::Microsoft {
186
-                account_id: account_id.to_string(),
187
-                calendar_id: calendar_id.to_string(),
188
-            }),
189
-            _ => None,
257
+        let provider_id = parts.next()?;
258
+        let account_id = parts.next()?;
259
+        let calendar_id = parts.next()?;
260
+        if provider_id.is_empty() || account_id.is_empty() || calendar_id.is_empty() {
261
+            return None;
190262
         }
263
+        Some(Self::provider(provider_id, account_id, calendar_id))
191264
     }
192265
 }
193266
 
@@ -440,8 +513,14 @@ impl Event {
440513
         self.source.source_id.starts_with("microsoft:")
441514
     }
442515
 
516
+    pub fn is_provider_backed(&self) -> bool {
517
+        EventWriteTargetId::from_event(self)
518
+            .as_ref()
519
+            .is_some_and(|target| !target.is_local())
520
+    }
521
+
443522
     pub fn is_editable(&self) -> bool {
444
-        self.is_local() || self.is_microsoft()
523
+        self.is_local() || self.is_provider_backed()
445524
     }
446525
 
447526
     pub const fn is_recurring_series(&self) -> bool {
@@ -728,7 +807,8 @@ impl ConfiguredAgendaSource {
728807
         draft: CreateEventDraft,
729808
         target: &EventWriteTargetId,
730809
     ) -> Result<Event, LocalEventStoreError> {
731
-        if let EventWriteTargetId::Microsoft { .. } = target
810
+        if let Some(provider_id) = target.provider_id() {
811
+            if provider_id == "microsoft"
732812
                 && let Some(microsoft) = &mut self.microsoft
733813
             {
734814
                 let http = ReqwestMicrosoftHttpClient;
@@ -737,10 +817,7 @@ impl ConfiguredAgendaSource {
737817
                     .create_event_in_target(draft, target, &http, &token_store)
738818
                     .map_err(provider_error);
739819
             }
740
-        if matches!(target, EventWriteTargetId::Microsoft { .. }) {
741
-            return Err(LocalEventStoreError::Provider {
742
-                reason: "Microsoft provider is not configured".to_string(),
743
-            });
820
+            return Err(provider_not_configured(provider_id));
744821
         }
745822
 
746823
         self.create_local_event(draft)
@@ -794,7 +871,9 @@ impl ConfiguredAgendaSource {
794871
         }
795872
 
796873
         if self.events.local_event_by_id(id).is_none()
797
-            && id.starts_with("microsoft:")
874
+            && let Some(provider_id) = event_id_provider(id)
875
+        {
876
+            if provider_id == "microsoft"
798877
                 && let Some(microsoft) = &mut self.microsoft
799878
             {
800879
                 let http = ReqwestMicrosoftHttpClient;
@@ -803,6 +882,8 @@ impl ConfiguredAgendaSource {
803882
                     .update_event(id, draft, &http, &token_store)
804883
                     .map_err(provider_error);
805884
             }
885
+            return Err(provider_not_configured(provider_id));
886
+        }
806887
 
807888
         let mut events = self.events.events().to_vec();
808889
         let Some(index) = events.iter().position(|event| event.id == id) else {
@@ -845,7 +926,9 @@ impl ConfiguredAgendaSource {
845926
         draft: CreateEventDraft,
846927
     ) -> Result<Event, LocalEventStoreError> {
847928
         if self.events.local_event_by_id(series_id).is_none()
848
-            && series_id.starts_with("microsoft:")
929
+            && let Some(provider_id) = event_id_provider(series_id)
930
+        {
931
+            if provider_id == "microsoft"
849932
                 && let Some(microsoft) = &mut self.microsoft
850933
             {
851934
                 let http = ReqwestMicrosoftHttpClient;
@@ -854,6 +937,8 @@ impl ConfiguredAgendaSource {
854937
                     .update_occurrence(series_id, anchor, draft, &http, &token_store)
855938
                     .map_err(provider_error);
856939
             }
940
+            return Err(provider_not_configured(provider_id));
941
+        }
857942
 
858943
         let mut events = self.events.events().to_vec();
859944
         let Some(index) = events.iter().position(|event| event.id == series_id) else {
@@ -906,7 +991,9 @@ impl ConfiguredAgendaSource {
906991
 
907992
     pub fn delete_event(&mut self, id: &str) -> Result<Event, LocalEventStoreError> {
908993
         if self.events.local_event_by_id(id).is_none()
909
-            && id.starts_with("microsoft:")
994
+            && let Some(provider_id) = event_id_provider(id)
995
+        {
996
+            if provider_id == "microsoft"
910997
                 && let Some(microsoft) = &mut self.microsoft
911998
             {
912999
                 let http = ReqwestMicrosoftHttpClient;
@@ -915,6 +1002,8 @@ impl ConfiguredAgendaSource {
9151002
                     .delete_event(id, &http, &token_store)
9161003
                     .map_err(provider_error);
9171004
             }
1005
+            return Err(provider_not_configured(provider_id));
1006
+        }
9181007
 
9191008
         let mut events = self.events.events().to_vec();
9201009
         let Some(index) = events.iter().position(|event| event.id == id) else {
@@ -934,7 +1023,9 @@ impl ConfiguredAgendaSource {
9341023
 
9351024
     pub fn duplicate_event(&mut self, id: &str) -> Result<Event, LocalEventStoreError> {
9361025
         if self.events.local_event_by_id(id).is_none()
937
-            && id.starts_with("microsoft:")
1026
+            && let Some(provider_id) = event_id_provider(id)
1027
+        {
1028
+            if provider_id == "microsoft"
9381029
                 && let Some(microsoft) = &mut self.microsoft
9391030
             {
9401031
                 let http = ReqwestMicrosoftHttpClient;
@@ -943,6 +1034,8 @@ impl ConfiguredAgendaSource {
9431034
                     .duplicate_event(id, &http, &token_store)
9441035
                     .map_err(provider_error);
9451036
             }
1037
+            return Err(provider_not_configured(provider_id));
1038
+        }
9461039
 
9471040
         let event = self
9481041
             .events
@@ -957,7 +1050,9 @@ impl ConfiguredAgendaSource {
9571050
         anchor: OccurrenceAnchor,
9581051
     ) -> Result<Event, LocalEventStoreError> {
9591052
         if self.events.local_event_by_id(series_id).is_none()
960
-            && series_id.starts_with("microsoft:")
1053
+            && let Some(provider_id) = event_id_provider(series_id)
1054
+        {
1055
+            if provider_id == "microsoft"
9611056
                 && let Some(microsoft) = &mut self.microsoft
9621057
             {
9631058
                 let http = ReqwestMicrosoftHttpClient;
@@ -966,6 +1061,8 @@ impl ConfiguredAgendaSource {
9661061
                     .duplicate_occurrence(series_id, anchor, &http, &token_store)
9671062
                     .map_err(provider_error);
9681063
             }
1064
+            return Err(provider_not_configured(provider_id));
1065
+        }
9691066
 
9701067
         let series = self.events.local_event_by_id(series_id).ok_or_else(|| {
9711068
             LocalEventStoreError::EventNotFound {
@@ -996,7 +1093,9 @@ impl ConfiguredAgendaSource {
9961093
         anchor: OccurrenceAnchor,
9971094
     ) -> Result<(), LocalEventStoreError> {
9981095
         if self.events.local_event_by_id(series_id).is_none()
999
-            && series_id.starts_with("microsoft:")
1096
+            && let Some(provider_id) = event_id_provider(series_id)
1097
+        {
1098
+            if provider_id == "microsoft"
10001099
                 && let Some(microsoft) = &mut self.microsoft
10011100
             {
10021101
                 let http = ReqwestMicrosoftHttpClient;
@@ -1005,6 +1104,8 @@ impl ConfiguredAgendaSource {
10051104
                     .delete_occurrence(series_id, anchor, &http, &token_store)
10061105
                     .map_err(provider_error);
10071106
             }
1107
+            return Err(provider_not_configured(provider_id));
1108
+        }
10081109
 
10091110
         let mut events = self.events.events().to_vec();
10101111
         let Some(index) = events.iter().position(|event| event.id == series_id) else {
@@ -1137,6 +1238,18 @@ fn provider_error(err: ProviderError) -> LocalEventStoreError {
11371238
     }
11381239
 }
11391240
 
1241
+fn provider_not_configured(provider_id: &str) -> LocalEventStoreError {
1242
+    LocalEventStoreError::Provider {
1243
+        reason: format!("provider '{provider_id}' is not configured"),
1244
+    }
1245
+}
1246
+
1247
+fn event_id_provider(id: &str) -> Option<&str> {
1248
+    id.split_once(':')
1249
+        .map(|(provider, _)| provider)
1250
+        .filter(|provider| !provider.is_empty())
1251
+}
1252
+
11401253
 #[derive(Debug)]
11411254
 pub enum HolidayProvider {
11421255
     Off(EmptyAgendaSource),
@@ -2989,6 +3102,74 @@ mod tests {
29893102
         Event::timed(id, title, start, end, source()).expect("valid timed event")
29903103
     }
29913104
 
3105
+    #[test]
3106
+    fn event_write_target_ids_support_provider_calendar_identity() {
3107
+        let target = EventWriteTargetId::provider("google", "personal", "primary");
3108
+
3109
+        assert!(!target.is_local());
3110
+        assert!(target.is_provider("google"));
3111
+        assert!(!target.is_microsoft());
3112
+        assert_eq!(target.provider_id(), Some("google"));
3113
+        assert_eq!(target.account_id(), Some("personal"));
3114
+        assert_eq!(target.calendar_id(), Some("primary"));
3115
+        assert_eq!(
3116
+            target.source_id().as_deref(),
3117
+            Some("google:personal:primary")
3118
+        );
3119
+    }
3120
+
3121
+    #[test]
3122
+    fn event_write_target_ids_are_recovered_from_provider_source_metadata() {
3123
+        let day = date(23);
3124
+        let event = Event::timed(
3125
+            "google:personal:primary:event-1",
3126
+            "Planning",
3127
+            at(day, 9, 0),
3128
+            at(day, 10, 0),
3129
+            SourceMetadata::new("google:personal:primary", "Google personal: Primary"),
3130
+        )
3131
+        .expect("valid provider event");
3132
+
3133
+        assert_eq!(
3134
+            EventWriteTargetId::from_event(&event),
3135
+            Some(EventWriteTargetId::provider(
3136
+                "google", "personal", "primary"
3137
+            ))
3138
+        );
3139
+        assert!(event.is_provider_backed());
3140
+        assert!(event.is_editable());
3141
+    }
3142
+
3143
+    #[test]
3144
+    fn configured_source_reports_unknown_provider_targets_clearly() {
3145
+        let day = date(23);
3146
+        let mut source =
3147
+            ConfiguredAgendaSource::new(InMemoryAgendaSource::new(), HolidayProvider::off());
3148
+        let draft = CreateEventDraft {
3149
+            title: "Planning".to_string(),
3150
+            timing: CreateEventTiming::Timed {
3151
+                start: at(day, 9, 0),
3152
+                end: at(day, 10, 0),
3153
+            },
3154
+            location: None,
3155
+            notes: None,
3156
+            reminders: Vec::new(),
3157
+            recurrence: None,
3158
+        };
3159
+
3160
+        let err = source
3161
+            .create_event_with_target(
3162
+                draft,
3163
+                &EventWriteTargetId::provider("google", "personal", "primary"),
3164
+            )
3165
+            .expect_err("unknown provider target fails");
3166
+
3167
+        assert!(
3168
+            err.to_string()
3169
+                .contains("provider 'google' is not configured")
3170
+        );
3171
+    }
3172
+
29923173
     #[test]
29933174
     fn agenda_construction_handles_empty_days() {
29943175
         let day = date(23);
src/providers.rsmodified
@@ -373,10 +373,10 @@ impl MicrosoftProviderRuntime {
373373
 
374374
     pub fn default_write_target(&self) -> Option<EventWriteTargetId> {
375375
         let (account, calendar_id) = self.config.default_calendar()?;
376
-        Some(EventWriteTargetId::Microsoft {
377
-            account_id: account.id.clone(),
378
-            calendar_id: calendar_id.to_string(),
379
-        })
376
+        Some(EventWriteTargetId::microsoft(
377
+            account.id.clone(),
378
+            calendar_id.to_string(),
379
+        ))
380380
     }
381381
 
382382
     pub fn status(&self, token_store: &dyn MicrosoftTokenStore) -> MicrosoftProviderStatus {
@@ -466,11 +466,7 @@ impl MicrosoftProviderRuntime {
466466
         http: &dyn MicrosoftHttpClient,
467467
         token_store: &dyn MicrosoftTokenStore,
468468
     ) -> Result<Event, ProviderError> {
469
-        let EventWriteTargetId::Microsoft {
470
-            account_id,
471
-            calendar_id,
472
-        } = target
473
-        else {
469
+        let Some((account_id, calendar_id)) = target.microsoft_parts() else {
474470
             return Err(ProviderError::Config(
475471
                 "Microsoft provider requires a Microsoft calendar target".to_string(),
476472
             ));
@@ -506,8 +502,8 @@ impl MicrosoftProviderRuntime {
506502
         let value = parse_graph_success_json(response)?;
507503
         let calendar =
508504
             fetch_calendar(http, &token, calendar_id).unwrap_or(MicrosoftCalendarRecord {
509
-                id: calendar_id.clone(),
510
-                name: calendar_id.clone(),
505
+                id: calendar_id.to_string(),
506
+                name: calendar_id.to_string(),
511507
                 can_edit: true,
512508
                 is_default: false,
513509
             });
@@ -2882,10 +2878,7 @@ mod tests {
28822878
         assert_eq!(targets[1].label, "Microsoft work: team");
28832879
         assert_eq!(
28842880
             runtime.default_write_target(),
2885
-            Some(EventWriteTargetId::Microsoft {
2886
-                account_id: "work".to_string(),
2887
-                calendar_id: "team".to_string(),
2888
-            })
2881
+            Some(EventWriteTargetId::microsoft("work", "team"))
28892882
         );
28902883
     }
28912884
 
@@ -2930,10 +2923,7 @@ mod tests {
29302923
             reminders: Vec::new(),
29312924
             recurrence: None,
29322925
         };
2933
-        let target = EventWriteTargetId::Microsoft {
2934
-            account_id: "work".to_string(),
2935
-            calendar_id: "personal".to_string(),
2936
-        };
2926
+        let target = EventWriteTargetId::microsoft("work", "personal");
29372927
 
29382928
         let event = runtime
29392929
             .create_event_in_target(draft, &target, &http, &store)