Skip to main content

libimagstore/
iter.rs

1//
2// imag - the personal information management suite for the commandline
3// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
4//
5// This library is free software; you can redistribute it and/or
6// modify it under the terms of the GNU Lesser General Public
7// License as published by the Free Software Foundation; version
8// 2.1 of the License.
9//
10// This library is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public
16// License along with this library; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18//
19
20macro_rules! mk_iterator_mod {
21    {
22        modname   = $modname:ident,
23        itername  = $itername:ident,
24        iteryield = $yield:ty,
25        extname   = $extname:ident,
26        extfnname = $extfnname:ident,
27        fun       = $fun:expr
28    } => {
29        pub mod $modname {
30            use crate::storeid::StoreId;
31            #[allow(unused_imports)]
32            use crate::store::FileLockEntry;
33            use crate::store::Store;
34            use failure::Fallible as Result;
35
36            pub struct $itername<'a>(Box<dyn Iterator<Item = Result<StoreId>> + 'a>, &'a Store);
37
38            impl<'a> $itername<'a>
39            {
40                pub fn new(inner: Box<dyn Iterator<Item = Result<StoreId>> + 'a>, store: &'a Store) -> Self {
41                    $itername(inner, store)
42                }
43            }
44
45            impl<'a> Iterator for $itername<'a>
46            {
47                type Item = Result<$yield>;
48
49                fn next(&mut self) -> Option<Self::Item> {
50                    self.0.next().map(|id| $fun(id?, self.1))
51                }
52            }
53
54            pub trait $extname<'a> {
55                fn $extfnname(self, store: &'a Store) -> $itername<'a>;
56            }
57
58            impl<'a, I> $extname<'a> for I
59                where I: Iterator<Item = Result<StoreId>> + 'a
60            {
61                fn $extfnname(self, store: &'a Store) -> $itername<'a> {
62                    $itername(Box::new(self), store)
63                }
64            }
65        }
66    }
67}
68
69mk_iterator_mod! {
70    modname   = create,
71    itername  = StoreCreateIterator,
72    iteryield = FileLockEntry<'a>,
73    extname   = StoreIdCreateIteratorExtension,
74    extfnname = into_create_iter,
75    fun       = |id: StoreId, store: &'a Store| store.create(id)
76}
77
78mk_iterator_mod! {
79    modname   = delete,
80    itername  = StoreDeleteIterator,
81    iteryield = (),
82    extname   = StoreIdDeleteIteratorExtension,
83    extfnname = into_delete_iter,
84    fun       = |id: StoreId, store: &'a Store| store.delete(id)
85}
86
87mk_iterator_mod! {
88    modname   = get,
89    itername  = StoreGetIterator,
90    iteryield = Option<FileLockEntry<'a>>,
91    extname   = StoreIdGetIteratorExtension,
92    extfnname = into_get_iter,
93    fun       = |id: StoreId, store: &'a Store| store.get(id)
94}
95
96mk_iterator_mod! {
97    modname   = retrieve,
98    itername  = StoreRetrieveIterator,
99    iteryield = FileLockEntry<'a>,
100    extname   = StoreIdRetrieveIteratorExtension,
101    extfnname = into_retrieve_iter,
102    fun       = |id: StoreId, store: &'a Store| store.retrieve(id)
103}
104
105#[cfg(test)]
106#[allow(dead_code)]
107mod compile_test {
108
109    // This module contains code to check whether this actually compiles the way we would like it to
110    // compile
111
112    use crate::store::Store;
113    use crate::storeid::StoreId;
114
115    fn store() -> Store {
116        unimplemented!("Not implemented because in compile-test")
117    }
118
119    fn test_compile_get() {
120        let store = store();
121        let _ = store
122            .entries()
123            .unwrap()
124            .into_get_iter();
125    }
126
127    fn test_compile_get_result() {
128        fn to_result(e: StoreId) -> Result<StoreId, ()> {
129            Ok(e)
130        }
131
132        let store = store();
133        let _ = store
134            .entries()
135            .unwrap()
136            .into_get_iter();
137    }
138}
139
140use crate::storeid::StoreId;
141use crate::storeid::StoreIdIterator;
142use self::delete::StoreDeleteIterator;
143use self::get::StoreGetIterator;
144use self::retrieve::StoreRetrieveIterator;
145use crate::file_abstraction::iter::PathIterator;
146use crate::store::Store;
147use failure::Fallible as Result;
148
149/// Iterator for iterating over all (or a subset of all) entries
150///
151/// The iterator now has functionality to optimize the iteration, if only a subdirectory of the
152/// store is required, for example `$STORE/foo`.
153///
154/// This is done via functionality where the underlying iterator gets
155/// altered.
156///
157/// As the (for the filesystem backend underlying) `walkdir::WalkDir` type is not as nice as it
158/// could be, iterating over two subdirectories with one iterator is not possible. Thus, iterators
159/// for two collections in the store should be build like this (untested):
160///
161/// ```ignore
162///     store
163///         .entries()?
164///         .in_collection("foo")?
165///         .chain(store.entries()?.in_collection("bar"))
166/// ```
167///
168/// Functionality to exclude subdirectories is not possible with the current implementation and has
169/// to be done during iteration, with filtering (as usual).
170pub struct Entries<'a>(PathIterator<'a>, &'a Store);
171
172impl<'a> Entries<'a> {
173
174    pub(crate) fn new(pi: PathIterator<'a>, store: &'a Store) -> Self {
175        Entries(pi, store)
176    }
177
178    pub fn in_collection(self, c: &str) -> Result<Self> {
179        Ok(Entries(self.0.in_collection(c)?, self.1))
180    }
181
182    /// Turn `Entries` iterator into generic `StoreIdIterator`
183    ///
184    /// # TODO
185    ///
186    /// Revisit whether this can be done in a cleaner way. See commit message for why this is
187    /// needed.
188    pub fn into_storeid_iter(self) -> StoreIdIterator {
189        use crate::storeid::StoreIdWithBase;
190        use crate::storeid::IntoStoreId;
191
192        let storepath = self.1.path().to_path_buf();
193
194        let iter = self.0
195            .into_inner()
196            .map(move |r| {
197                r.and_then(|path| {
198                    StoreIdWithBase::from_full_path(&storepath, path)?.into_storeid()
199                })
200            });
201        StoreIdIterator::new(Box::new(iter))
202    }
203
204    /// Transform the iterator into a StoreDeleteIterator
205    ///
206    /// This immitates the API from `libimagstore::iter`.
207    pub fn into_delete_iter(self) -> StoreDeleteIterator<'a> {
208        StoreDeleteIterator::new(Box::new(self.0.map(|r| r.map(|id| id.without_base()))), self.1)
209    }
210
211    /// Transform the iterator into a StoreGetIterator
212    ///
213    /// This immitates the API from `libimagstore::iter`.
214    pub fn into_get_iter(self) -> StoreGetIterator<'a> {
215        StoreGetIterator::new(Box::new(self.0.map(|r| r.map(|id| id.without_base()))), self.1)
216    }
217
218    /// Transform the iterator into a StoreRetrieveIterator
219    ///
220    /// This immitates the API from `libimagstore::iter`.
221    pub fn into_retrieve_iter(self) -> StoreRetrieveIterator<'a> {
222        StoreRetrieveIterator::new(Box::new(self.0.map(|r| r.map(|id| id.without_base()))), self.1)
223    }
224
225    /// Find entries where the id contains a substring
226    ///
227    /// This is useful for finding entries if the user supplied only a part of the ID, for example
228    /// if the ID contains a UUID where the user did not specify the full UUID, E.G.:
229    ///
230    /// ```ignore
231    ///     imag foo show 827d8596-fad1-4
232    /// ```
233    ///
234    /// # Note
235    ///
236    /// The substring match is done with `contains()`.
237    ///
238    pub fn find_by_id_substr<'b>(self, id_substr: &'b str) -> FindContains<'a, 'b> {
239        FindContains(self, id_substr)
240    }
241
242    /// Find entries where the id starts with a substring
243    ///
244    /// Same as `Entries::find_by_id_substr()`, but using `starts_with()` rather than `contains`.
245    ///
246    pub fn find_by_id_startswith<'b>(self, id_substr: &'b str) -> FindStartsWith<'a, 'b> {
247        FindStartsWith(self, id_substr)
248    }
249
250}
251
252impl<'a> Iterator for Entries<'a> {
253    type Item = Result<StoreId>;
254
255    fn next(&mut self) -> Option<Self::Item> {
256        self.0.next().map(|r| r.map(|id| id.without_base()))
257    }
258}
259
260pub struct FindContains<'a, 'b>(Entries<'a>, &'b str);
261
262impl<'a, 'b> Iterator for FindContains<'a, 'b> {
263    type Item = Result<StoreId>;
264
265    fn next(&mut self) -> Option<Self::Item> {
266        loop {
267            match self.0.next() {
268                None           => return None,
269                Some(Err(e))   => return Some(Err(e)),
270                Some(Ok(next)) => if next.local().to_string_lossy().contains(self.1) {
271                    return Some(Ok(next))
272                }, // else loop
273            }
274        }
275    }
276}
277
278pub struct FindStartsWith<'a, 'b>(Entries<'a>, &'b str);
279
280impl<'a, 'b> Iterator for FindStartsWith<'a, 'b> {
281    type Item = Result<StoreId>;
282
283    fn next(&mut self) -> Option<Self::Item> {
284        loop {
285            match self.0.next() {
286                None           => return None,
287                Some(Err(e))   => return Some(Err(e)),
288                Some(Ok(next)) => if next.local().to_string_lossy().starts_with(self.1) {
289                    return Some(Ok(next))
290                }, // else loop
291            }
292        }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    extern crate env_logger;
299
300    use std::path::PathBuf;
301    use std::sync::Arc;
302
303    fn setup_logging() {
304        let _ = env_logger::try_init();
305    }
306
307    use crate::store::Store;
308    use crate::storeid::StoreId;
309    use crate::file_abstraction::inmemory::InMemoryFileAbstraction;
310    use libimagutil::variants::generate_variants;
311
312    pub fn get_store() -> Store {
313        let backend = Arc::new(InMemoryFileAbstraction::default());
314        Store::new_with_backend(PathBuf::from("/"), &None, backend).unwrap()
315    }
316
317    #[test]
318    fn test_entries_iterator_in_collection() {
319        setup_logging();
320        let store = get_store();
321
322        let ids = {
323            let base = String::from("entry");
324            let variants = vec!["coll_1", "coll_2", "coll_3"];
325            let modifier = |base: &String, v: &&str| {
326                StoreId::new(PathBuf::from(format!("{}/{}", *v, base))).unwrap()
327            };
328
329            generate_variants(&base, variants.iter(), &modifier)
330        };
331
332        for id in ids {
333            let _ = store.retrieve(id).unwrap();
334        }
335
336        let succeeded = store.entries()
337            .unwrap()
338            .in_collection("coll_3")
339            .unwrap()
340            .map(|id| { debug!("Processing id = {:?}", id); id })
341            .all(|id| id.unwrap().is_in_collection(&["coll_3"]));
342
343        assert!(succeeded, "not all entries in iterator are from coll_3 collection");
344    }
345
346    #[test]
347    fn test_entries_iterator_substr() {
348        setup_logging();
349        let store = get_store();
350
351        let ids = {
352            let base = String::from("entry");
353            let variants = vec!["coll_1", "coll2", "coll_3"];
354            let modifier = |base: &String, v: &&str| {
355                StoreId::new(PathBuf::from(format!("{}/{}", *v, base))).unwrap()
356            };
357
358            generate_variants(&base, variants.iter(), &modifier)
359        };
360
361        for id in ids {
362            let _ = store.retrieve(id).unwrap();
363        }
364
365        let succeeded = store.entries()
366            .unwrap()
367            .find_by_id_substr("_")
368            .map(|id| { debug!("Processing id = {:?}", id); id })
369            .all(|id| id.unwrap().local_display_string().contains('_'));
370
371        assert!(succeeded, "not all entries in iterator contain '_'");
372    }
373
374    #[test]
375    fn test_entries_iterator_startswith() {
376        setup_logging();
377        let store = get_store();
378
379        let ids = {
380            let base = String::from("entry");
381            let variants = vec!["coll_1", "coll2", "coll_3"];
382            let modifier = |base: &String, v: &&str| {
383                StoreId::new(PathBuf::from(format!("{}/{}", *v, base))).unwrap()
384            };
385
386            generate_variants(&base, variants.iter(), &modifier)
387        };
388
389        for id in ids {
390            let _ = store.retrieve(id).unwrap();
391        }
392
393        let succeeded = store.entries()
394            .unwrap()
395            .find_by_id_startswith("entr")
396            .map(|id| { debug!("Processing id = {:?}", id); id })
397            .all(|id| id.unwrap().local_display_string().starts_with("entry"));
398
399        assert!(succeeded, "not all entries in iterator start with 'entr'");
400    }
401
402}
403