sioul_core/
capabilities.rs1use crate::config::{Account, AccountKind};
14use crate::tasks::Task;
15use crate::vdir::{Collection, Kind};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Provider {
20 Local,
22 Dav,
24 Google,
26 GoogleTasks,
28}
29
30pub fn provider_of(account: Option<&Account>) -> Provider {
32 match account {
33 None => Provider::Local,
34 Some(a) if a.auth.as_deref() == Some("google") => Provider::Google,
35 Some(a) if a.host.as_deref().is_some_and(|h| h.ends_with("googleusercontent.com") || h.ends_with("googleapis.com") || h == "www.google.com") => Provider::Google,
36 Some(a) if a.kind == AccountKind::Dav => Provider::Dav,
37 Some(_) => Provider::Local,
38 }
39}
40
41pub fn provider_of_collection(account: Option<&Account>, collection: &Collection) -> Provider {
43 match provider_of(account) {
44 Provider::Google if collection.kind == Kind::Calendars && collection.components.iter().any(|c| c.eq_ignore_ascii_case("VTODO")) => Provider::GoogleTasks,
45 provider => provider,
46 }
47}
48
49pub fn task_fields_lost(provider: Provider, is_step: bool) -> Vec<&'static str> {
52 if provider != Provider::GoogleTasks {
53 return Vec::new();
54 }
55 let mut lost = vec!["start", "estimate", "project", "kind", "office", "categories", "billable", "energy", "repeat", "waits", "margins", "costs"];
56 if is_step {
57 lost.push("steps");
58 }
59 lost
60}
61
62pub fn makes_collections(provider: Provider) -> bool {
65 !matches!(provider, Provider::Google)
66}
67
68pub fn task_losses(task: &Task, to: Provider) -> Vec<&'static str> {
71 if to != Provider::GoogleTasks {
72 return Vec::new();
73 }
74 let mut lost = Vec::new();
75 let mut add = |used: bool, what: &'static str| {
76 if used {
77 lost.push(what);
78 }
79 };
80 add(task.relations.iter().any(|r| !r.kind.is_empty() && !r.kind.eq_ignore_ascii_case("PARENT") && !r.kind.eq_ignore_ascii_case("CHILD")), "waits");
81 add(!task.start.is_empty(), "start");
82 add(task.due.contains('T'), "due-time");
83 add(!task.repeat.is_empty(), "repeat");
84 add(task.priority > 0, "priority");
85 add(!task.kind.is_empty() || task.office_hours, "kind");
86 add(task.estimate > 0, "estimate");
87 add(!task.links.is_empty(), "links");
88 add(!task.contacts.is_empty(), "contacts");
89 add(!task.categories.is_empty(), "categories");
90 add(task.billable.is_some(), "billable");
91 add(!task.energy.is_empty(), "energy");
92 add(!task.margins.is_empty(), "margins");
93 add(task.demands != crate::demands::Demands::default(), "costs");
94 lost
95}
96
97pub fn contact_losses(card: &str, to: Provider) -> Vec<&'static str> {
101 if to != Provider::Google {
102 return Vec::new();
103 }
104 let lines: Vec<String> = crate::lines::unfold(card).into_iter().map(|l| l.to_ascii_uppercase()).collect();
105 let has = |test: &dyn Fn(&str) -> bool| lines.iter().any(|l| test(l));
106 let mut lost = Vec::new();
107 if has(&|l| l.contains("X-ABLABEL")) {
108 lost.push("labels");
109 }
110 if has(&|l| l.starts_with("BDAY") && l.split_once(':').is_some_and(|(_, v)| v.starts_with("--"))) {
111 lost.push("birthday-no-year");
112 }
113 if has(&|l| ["KIND", "GENDER", "ANNIVERSARY", "MEMBER", "RELATED", "LANG"].iter().any(|p| l.starts_with(&format!("{p}:")) || l.starts_with(&format!("{p};")))) {
114 lost.push("vcard4");
115 }
116 lost
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn google_keeps_less() {
125 let card = "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Jane\r\nitem1.EMAIL:jane@example.org\r\nitem1.X-ABLabel:Studio\r\nBDAY:--0412\r\nGENDER:F\r\nEND:VCARD\r\n";
126 assert_eq!(contact_losses(card, Provider::Google), vec!["labels", "birthday-no-year", "vcard4"]);
127 assert!(contact_losses(card, Provider::Dav).is_empty());
128 let task = Task { repeat: "weekly".into(), estimate: 30, ..Task::default() };
129 assert_eq!(task_losses(&task, Provider::GoogleTasks), vec!["repeat", "estimate"]);
130 assert!(task_losses(&task, Provider::Dav).is_empty());
131 assert!(!makes_collections(Provider::Google) && makes_collections(Provider::Dav) && makes_collections(Provider::GoogleTasks));
132 assert!(task_fields_lost(Provider::GoogleTasks, true).contains(&"steps") && task_fields_lost(Provider::Dav, true).is_empty());
133 }
134}