omni_dev/drive/docs/
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 DocTarget {
26 Document(Box<DriveFile>),
29 Shortcut {
32 name: String,
34 },
35 NotADocument {
37 name: String,
39 mime_type: String,
41 },
42}
43
44impl DocTarget {
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_DOC_MIME_TYPE {
59 return Self::Document(Box::new(file));
60 }
61 Self::NotADocument {
62 name: file.name,
63 mime_type: file.mime_type,
64 }
65 }
66
67 #[must_use]
69 pub fn document(&self) -> Option<&DriveFile> {
70 match self {
71 Self::Document(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::Document(_) => None,
84 Self::Shortcut { name } => Some(format!(
85 "Refused: '{name}' is a shortcut; `drive docs {verb}` doesn't follow shortcuts — \
86 resolve the target document's id and use that instead"
87 )),
88 Self::NotADocument { name, mime_type } => {
89 let base = format!(
90 "Refused: '{name}' is not a Google Doc (mimeType: {mime_type}); \
91 `drive docs {verb}` only works on Google Docs"
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_SLIDES_MIME_TYPE => {
101 Some(format!("{base} — try `omni-dev drive slides read` instead"))
102 }
103 _ => Some(base),
104 }
105 }
106 }
107 }
108}
109
110pub async fn classify(api: &FilesApi<'_>, file_id: &str) -> Result<DocTarget> {
112 Ok(DocTarget::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_google_doc() {
157 let target = DocTarget::of(file("Design Doc", GOOGLE_DOC_MIME_TYPE));
158 assert!(target.refusal_message("read").is_none());
159 assert_eq!(target.document().unwrap().name, "Design Doc");
160 }
161
162 #[test]
165 fn classify_checks_shortcut_before_the_mime_check() {
166 let target = DocTarget::of(file("Design Doc (link)", GOOGLE_SHORTCUT_MIME_TYPE));
167 assert!(matches!(target, DocTarget::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!(!message.contains("not a Google Doc"), "{message}");
172 }
173
174 #[test]
175 fn refusal_for_a_spreadsheet_points_at_drive_sheets_read() {
176 let message = DocTarget::of(file("Q3", GOOGLE_SHEET_MIME_TYPE))
177 .refusal_message("read")
178 .unwrap();
179 assert!(message.contains("not a Google Doc"), "{message}");
180 assert!(message.contains("drive sheets read"), "{message}");
181 }
182
183 #[test]
184 fn refusal_for_a_presentation_points_at_drive_slides() {
185 let message = DocTarget::of(file("Deck", GOOGLE_SLIDES_MIME_TYPE))
186 .refusal_message("read")
187 .unwrap();
188 assert!(message.contains("drive slides read"), "{message}");
189 }
190
191 #[test]
192 fn refusal_for_a_plain_file_names_its_mime_type_without_a_hint() {
193 let message = DocTarget::of(file("notes.txt", "text/plain"))
194 .refusal_message("read")
195 .unwrap();
196 assert!(message.contains("text/plain"), "{message}");
197 assert!(!message.contains("try `omni-dev"), "{message}");
198 }
199
200 #[test]
202 fn refusal_names_the_verb() {
203 let target = DocTarget::of(file("Q3", GOOGLE_SHEET_MIME_TYPE));
204 assert!(target
205 .refusal_message("info")
206 .unwrap()
207 .contains("drive docs info"));
208 assert!(target
209 .refusal_message("read")
210 .unwrap()
211 .contains("drive docs read"));
212 }
213}