1mod calendar;
2mod doing;
3mod json;
4mod timing;
5
6use std::path::Path;
7
8use doing_error::Result;
9use doing_taskpaper::Entry;
10use regex::Regex;
11
12pub trait ImportPlugin {
17 fn import(&self, path: &Path) -> Result<Vec<Entry>>;
19
20 fn name(&self) -> &str;
22
23 fn settings(&self) -> ImportPluginSettings;
25}
26
27#[derive(Clone, Debug)]
29pub struct ImportPluginSettings {
30 pub trigger: String,
31}
32
33pub struct ImportRegistry {
39 plugins: Vec<RegisteredPlugin>,
40}
41
42impl ImportRegistry {
43 pub fn new() -> Self {
45 Self {
46 plugins: Vec::new(),
47 }
48 }
49
50 pub fn available_formats(&self) -> Vec<&str> {
52 let mut names: Vec<&str> = self.plugins.iter().map(|p| p.name.as_str()).collect();
53 names.sort();
54 names
55 }
56
57 pub fn register(&mut self, plugin: Box<dyn ImportPlugin>) {
66 let name = plugin.name().to_string();
67 let settings = plugin.settings();
68 let pattern = settings.trigger.trim().to_string();
69 let trigger = Regex::new(&format!("(?i)^(?:{pattern})$"))
70 .unwrap_or_else(|_| panic!("invalid trigger pattern for plugin \"{name}\": {pattern}"));
71 self.plugins.push(RegisteredPlugin {
72 name,
73 plugin,
74 trigger,
75 });
76 }
77
78 pub fn resolve(&self, format: &str) -> Option<&dyn ImportPlugin> {
83 self
84 .plugins
85 .iter()
86 .find(|p| p.trigger.is_match(format))
87 .map(|p| p.plugin.as_ref())
88 }
89}
90
91impl Default for ImportRegistry {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97struct RegisteredPlugin {
98 name: String,
99 plugin: Box<dyn ImportPlugin>,
100 trigger: Regex,
101}
102
103pub fn default_registry() -> ImportRegistry {
105 let mut registry = ImportRegistry::new();
106 registry.register(Box::new(calendar::CalendarImport));
107 registry.register(Box::new(doing::DoingImport));
108 registry.register(Box::new(json::JsonImport));
109 registry.register(Box::new(timing::TimingImport));
110 registry
111}
112
113#[cfg(test)]
114mod test {
115 use std::path::Path;
116
117 use super::*;
118
119 struct MockPlugin {
120 name: String,
121 trigger: String,
122 }
123
124 impl MockPlugin {
125 fn new(name: &str, trigger: &str) -> Self {
126 Self {
127 name: name.into(),
128 trigger: trigger.into(),
129 }
130 }
131 }
132
133 impl ImportPlugin for MockPlugin {
134 fn import(&self, _path: &Path) -> Result<Vec<Entry>> {
135 Ok(Vec::new())
136 }
137
138 fn name(&self) -> &str {
139 &self.name
140 }
141
142 fn settings(&self) -> ImportPluginSettings {
143 ImportPluginSettings {
144 trigger: self.trigger.clone(),
145 }
146 }
147 }
148
149 mod default_registry {
150 use pretty_assertions::assert_eq;
151
152 use super::*;
153
154 #[test]
155 fn it_registers_all_built_in_plugins() {
156 let registry = default_registry();
157
158 assert_eq!(
159 registry.available_formats(),
160 vec!["calendar", "doing", "json", "timing"]
161 );
162 }
163 }
164
165 mod import_registry_available_formats {
166 use pretty_assertions::assert_eq;
167
168 use super::*;
169
170 #[test]
171 fn it_returns_empty_for_new_registry() {
172 let registry = ImportRegistry::new();
173
174 assert!(registry.available_formats().is_empty());
175 }
176
177 #[test]
178 fn it_returns_sorted_format_names() {
179 let mut registry = ImportRegistry::new();
180 registry.register(Box::new(MockPlugin::new("timing", "timing")));
181 registry.register(Box::new(MockPlugin::new("doing", "doing")));
182
183 let formats = registry.available_formats();
184
185 assert_eq!(formats, vec!["doing", "timing"]);
186 }
187 }
188
189 mod import_registry_register {
190 use pretty_assertions::assert_eq;
191
192 use super::*;
193
194 #[test]
195 fn it_adds_plugin_to_registry() {
196 let mut registry = ImportRegistry::new();
197
198 registry.register(Box::new(MockPlugin::new("doing", "doing")));
199
200 assert_eq!(registry.available_formats(), vec!["doing"]);
201 }
202
203 #[test]
204 #[should_panic(expected = "invalid trigger pattern")]
205 fn it_panics_on_invalid_trigger_pattern() {
206 let mut registry = ImportRegistry::new();
207
208 registry.register(Box::new(MockPlugin::new("bad", "(?invalid")));
209 }
210 }
211
212 mod import_registry_resolve {
213 use pretty_assertions::assert_eq;
214
215 use super::*;
216
217 #[test]
218 fn it_matches_exact_format_name() {
219 let mut registry = ImportRegistry::new();
220 registry.register(Box::new(MockPlugin::new("doing", "doing")));
221
222 let plugin = registry.resolve("doing").unwrap();
223
224 assert_eq!(plugin.name(), "doing");
225 }
226
227 #[test]
228 fn it_matches_case_insensitively() {
229 let mut registry = ImportRegistry::new();
230 registry.register(Box::new(MockPlugin::new("doing", "doing")));
231
232 assert!(registry.resolve("DOING").is_some());
233 assert!(registry.resolve("Doing").is_some());
234 }
235
236 #[test]
237 fn it_returns_none_for_unknown_format() {
238 let mut registry = ImportRegistry::new();
239 registry.register(Box::new(MockPlugin::new("doing", "doing")));
240
241 assert!(registry.resolve("csv").is_none());
242 }
243
244 #[test]
245 fn it_does_not_match_partial_strings() {
246 let mut registry = ImportRegistry::new();
247 registry.register(Box::new(MockPlugin::new("doing", "doing")));
248
249 assert!(registry.resolve("doingx").is_none());
250 assert!(registry.resolve("xdoing").is_none());
251 }
252 }
253}