cromulent 0.1.1

A safe wrapper around `wordexp-sys`.
Documentation
//! Convenience types for when you expect your words to be valid paths.

use std::os::unix::ffi::OsStrExt;

use super::WordList as WordListCStr;

/// A slice-like type representing the expanded words.
///
/// Elements of this "slice" are [`&Path`](std::path::Path).
///
/// # Examples
///
/// All examples will use the following expansion:
///
/// ```
/// let words = cromulent::WordExpander::default()
///     .allow_commands()
///     .expand("tests/*")?;
/// let path = cromulent::path::WordList::from(&words);
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Length-checking:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("tests/*")?;
/// # let path = cromulent::path::WordList::from(&words);
///
/// assert!(!path.is_empty());
/// assert_eq!(path.len(), 3);
///
/// let _ = &path[2];
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Indexing:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("tests/*")?;
/// # let path = cromulent::path::WordList::from(&words);
///
/// let _ = &path[0];
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Iteration:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("tests/*")?;
/// # let path = cromulent::path::WordList::from(&words);
///
/// let paths = ["tests/lib.rs", "tests/path.rs", "tests/utf8.rs"]
///     .iter()
///     .map(std::path::Path::new)
///     .collect::<Vec<_>>();
///
/// for path in &path {
///     assert!(paths.contains(&path));
/// }
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WordList<'p, 'l: 'p>(&'p WordListCStr<'l>);

impl<'p, 'l: 'p> From<&'p WordListCStr<'l>> for WordList<'p, 'l> {
    fn from(word_list: &'p WordListCStr<'l>) -> Self {
        WordList(word_list)
    }
}

impl<'p, 'l: 'p> std::ops::Index<usize> for WordList<'p, 'l> {
    type Output = std::path::Path;

    fn index(&self, index: usize) -> &'p Self::Output {
        std::ffi::OsStr::from_bytes(self.0[index].to_bytes()).as_ref()
    }
}

impl<'p, 'l: 'p> WordList<'p, 'l> {
    /// Create a `WordList`.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("tests/lib.rs")?;
    /// assert_eq!(&words[0], &*std::ffi::CString::new("tests/lib.rs").unwrap());
    ///
    /// let path = cromulent::path::WordList::new(&words);
    /// assert_eq!(&path[0], std::path::Path::new("tests/lib.rs"));
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn new(list: &'p WordListCStr<'l>) -> WordList<'p, 'l> {
        list.into()
    }

    /// Return `true` if no words were expanded.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("")?;
    /// let path = cromulent::path::WordList::from(&words);
    /// assert!(path.is_empty());
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Return the number of words that the input expanded into.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("")?;
    /// let path = cromulent::path::WordList::from(&words);
    /// assert_eq!(path.len(), 0);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("tests/*")?;
    /// let path = cromulent::path::WordList::from(&words);
    /// assert_eq!(path.len(), 3);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn len(&self) -> usize {
        self.0.len()
    }

    /// Return the word at a given index without panicking.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("tests/*")?;
    /// let path = cromulent::path::WordList::from(&words);
    ///
    /// assert!(path.get(1).is_some());
    /// assert!(path.get(3).is_none());
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn get(&self, index: usize) -> Option<&<Self as std::ops::Index<usize>>::Output> {
        if index < self.len() {
            Some(&self[index])
        } else {
            None
        }
    }

    /// Return the word at a given index without checks.
    ///
    /// # Safety
    ///
    /// Callers of this function are responsible for ensuring that `index < self.len()`.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(seq 3)")?;
    /// let path = words.path();
    ///
    /// assert_eq!(unsafe { path.get_unchecked(1) }, &path[1]);
    /// // DANGER!
    /// // path.get_unchecked(3);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub unsafe fn get_unchecked(&self, index: usize) -> &<Self as std::ops::Index<usize>>::Output {
        let element = self.0.get_unchecked(index);
        std::ffi::OsStr::from_bytes(element.to_bytes()).as_ref()
    }
}

pub struct Iter<'r, 'p: 'r, 'l: 'p> {
    word_list: &'r WordList<'p, 'l>,
    index: usize,
}

impl<'r, 'p: 'r, 'l: 'p> IntoIterator for &'r WordList<'p, 'l> {
    type IntoIter = Iter<'r, 'p, 'l>;
    type Item = <Self::IntoIter as Iterator>::Item;

    fn into_iter(self) -> Self::IntoIter {
        Iter {
            word_list: self,
            index: 0,
        }
    }
}

impl<'r, 'p: 'r, 'l: 'p> Iterator for Iter<'r, 'p, 'l> {
    type Item = &'r <WordList<'p, 'l> as std::ops::Index<usize>>::Output;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index >= self.word_list.len() {
            None
        } else {
            let result = &self.word_list[self.index];
            self.index += 1;
            Some(result)
        }
    }
}