omni_dev/drive/slides/
target.rs1use anyhow::Result;
16
17use crate::drive::files_api::FilesApi;
18use crate::drive::types::{
19 DriveFile, GOOGLE_DOC_MIME_TYPE, GOOGLE_SHEET_MIME_TYPE, GOOGLE_SHORTCUT_MIME_TYPE,
20 GOOGLE_SLIDES_MIME_TYPE,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum SlidesTarget {
26 Presentation(Box<DriveFile>),
29 Shortcut {
32 name: String,
34 },
35 NotAPresentation {
37 name: String,
39 mime_type: String,
41 },
42}
43
44impl SlidesTarget {
45 #[must_use]
51 pub fn of(file: DriveFile) -> Self {
52 if file.mime_type == GOOGLE_SHORTCUT_MIME_TYPE {
56 return Self::Shortcut { name: file.name };
57 }
58 if file.mime_type == GOOGLE_SLIDES_MIME_TYPE {
59 return Self::Presentation(Box::new(file));
60 }
61 Self::NotAPresentation {
62 name: file.name,
63 mime_type: file.mime_type,
64 }
65 }
66
67 #[must_use]
69 pub fn presentation(&self) -> Option<&DriveFile> {
70 match self {
71 Self::Presentation(file) => Some(file),
72 _ => None,
73 }
74 }
75
76 #[must_use]
81 pub fn refusal_message(&self, verb: &str) -> Option<String> {
82 match self {
83 Self::Presentation(_) => None,
84 Self::Shortcut { name } => Some(format!(
85 "Refused: '{name}' is a shortcut; `drive slides {verb}` doesn't follow shortcuts — \
86 resolve the target presentation's id and use that instead"
87 )),
88 Self::NotAPresentation { name, mime_type } => {
89 let base = format!(
90 "Refused: '{name}' is not a Google Slides presentation (mimeType: {mime_type}); \
91 `drive slides {verb}` only works on Google Slides"
92 );
93 match mime_type.as_str() {
97 GOOGLE_SHEET_MIME_TYPE => {
98 Some(format!("{base} — try `omni-dev drive sheets read` instead"))
99 }
100 GOOGLE_DOC_MIME_TYPE => {
101 Some(format!("{base} — try `omni-dev drive docs read` instead"))
102 }
103 _ => Some(base),
104 }
105 }
106 }
107 }
108}
109
110pub async fn classify(api: &FilesApi<'_>, file_id: &str) -> Result<SlidesTarget> {
112 Ok(SlidesTarget::of(api.get_metadata(file_id).await?))
113}
114
115pub async fn explain_failure(
128 api: &FilesApi<'_>,
129 file_id: &str,
130 verb: &str,
131 err: anyhow::Error,
132) -> anyhow::Error {
133 match classify(api, file_id).await {
134 Ok(target) => target
135 .refusal_message(verb)
136 .map_or(err, |message| anyhow::anyhow!(message)),
137 Err(_) => err,
138 }
139}
140
141#[cfg(test)]
142#[allow(clippy::unwrap_used, clippy::expect_used)]
143mod tests {
144 use super::*;
145
146 fn file(name: &str, mime_type: &str) -> DriveFile {
147 DriveFile {
148 id: "d1".to_string(),
149 name: name.to_string(),
150 mime_type: mime_type.to_string(),
151 ..DriveFile::default()
152 }
153 }
154
155 #[test]
156 fn classify_recognises_a_presentation() {
157 let target = SlidesTarget::of(file("Deck", GOOGLE_SLIDES_MIME_TYPE));
158 assert!(target.refusal_message("read").is_none());
159 assert_eq!(target.presentation().unwrap().name, "Deck");
160 }
161
162 #[test]
165 fn classify_checks_shortcut_before_the_mime_check() {
166 let target = SlidesTarget::of(file("Deck (link)", GOOGLE_SHORTCUT_MIME_TYPE));
167 assert!(matches!(target, SlidesTarget::Shortcut { .. }));
168 let message = target.refusal_message("read").unwrap();
169 assert!(message.contains("is a shortcut"), "{message}");
170 assert!(message.contains("doesn't follow shortcuts"), "{message}");
171 assert!(
172 !message.contains("not a Google Slides presentation"),
173 "{message}"
174 );
175 }
176
177 #[test]
178 fn refusal_for_a_spreadsheet_points_at_drive_sheets_read() {
179 let message = SlidesTarget::of(file("Q3", GOOGLE_SHEET_MIME_TYPE))
180 .refusal_message("read")
181 .unwrap();
182 assert!(
183 message.contains("not a Google Slides presentation"),
184 "{message}"
185 );
186 assert!(message.contains("drive sheets read"), "{message}");
187 }
188
189 #[test]
190 fn refusal_for_a_doc_points_at_drive_docs() {
191 let message = SlidesTarget::of(file("Deck", GOOGLE_DOC_MIME_TYPE))
192 .refusal_message("read")
193 .unwrap();
194 assert!(message.contains("drive docs read"), "{message}");
195 }
196
197 #[test]
198 fn refusal_for_a_plain_file_names_its_mime_type_without_a_hint() {
199 let message = SlidesTarget::of(file("notes.txt", "text/plain"))
200 .refusal_message("read")
201 .unwrap();
202 assert!(message.contains("text/plain"), "{message}");
203 assert!(!message.contains("try `omni-dev"), "{message}");
204 }
205
206 #[test]
208 fn refusal_names_the_verb() {
209 let target = SlidesTarget::of(file("Q3", GOOGLE_SHEET_MIME_TYPE));
210 assert!(target
211 .refusal_message("info")
212 .unwrap()
213 .contains("drive slides info"));
214 assert!(target
215 .refusal_message("read")
216 .unwrap()
217 .contains("drive slides read"));
218 }
219}