cromulent 0.1.1

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

use super::WordList as WordListCStr;

/// A slice-like type representing the expanded words.
///
/// Elements of this "slice" are `&str`.
///
/// The validity of the UTF-8 is checked lazily, when you retrieve the element.
/// Invalid UTF-8 will cause a panic, if that it unacceptable then you should
/// just use [WordListCStr] and manually convert.
///
/// # Examples
///
/// All examples will use the following expansion:
///
/// ```
/// let words = cromulent::WordExpander::default()
///     .allow_commands()
///     .expand("$(seq 3)")?;
/// let utf8 = cromulent::utf8::WordList::from(&words);
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Length-checking:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("$(seq 3)")?;
/// # let utf8 = cromulent::utf8::WordList::from(&words);
///
/// assert!(!utf8.is_empty());
/// assert_eq!(utf8.len(), 3);
/// assert_eq!(&utf8[0], "1");
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Indexing:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("$(seq 3)")?;
/// # let utf8 = cromulent::utf8::WordList::from(&words);
///
/// assert_eq!(&utf8[0], "1");
///
/// # Ok::<(), cromulent::WordError>(())
/// ```
///
/// Iteration:
///
/// ```
/// # let words = cromulent::WordExpander::default()
/// #     .allow_commands()
/// #     .expand("$(seq 3)")?;
/// # let utf8 = cromulent::utf8::WordList::from(&words);
///
/// for (i, word) in utf8.into_iter().enumerate() {
///     assert_eq!(word.parse::<usize>()?, i + 1)
/// }
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WordList<'u, 'l: 'u>(&'u WordListCStr<'l>);

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

impl<'u, 'l: 'u> std::ops::Index<usize> for WordList<'u, 'l> {
    type Output = str;

    fn index(&self, index: usize) -> &'u Self::Output {
        match self.0[index].to_str() {
            Ok(s) => s,
            // Explicitly panic (as opposed to `expect`) because this is an "API panic" rather than
            // a "can't happen" panic.
            Err(e) => panic!("invalid utf-8 in word: {}", e),
        }
    }
}

impl<'u, 'l: 'u> WordList<'u, 'l> {
    /// Create a lazily checked `WordList`.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("what a nice sentence")?;
    /// assert_eq!(&words[0], &*std::ffi::CString::new("what").unwrap());
    ///
    /// let utf8 = cromulent::utf8::WordList::new(&words);
    /// assert_eq!(&utf8[0], "what");
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    ///
    /// ```
    /// let invalid =
    ///     cromulent::WordExpander::default().expand_cstr(&std::ffi::CString::new(vec![243, 222])?);
    /// assert!(invalid.is_ok());
    ///
    /// let invalid = invalid.unwrap();
    /// let invalid = cromulent::utf8::WordList::new(&invalid);
    /// let result = std::panic::catch_unwind(|| &invalid[0]);
    /// assert!(result.is_err());
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    pub fn new(list: &'u WordListCStr<'l>) -> Self {
        list.into()
    }

    /// Validate and create a `WordList`.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("what a nice sentence")?;
    /// assert_eq!(&words[0], &*std::ffi::CString::new("what").unwrap());
    ///
    /// let utf8 = cromulent::utf8::WordList::new_eager(&words)?;
    /// assert_eq!(&utf8[0], "what");
    ///
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    ///
    /// ```
    /// let invalid = cromulent::WordExpander::default().expand_cstr(&std::ffi::CString::new(vec![243, 222])?);
    /// assert!(invalid.is_ok());
    ///
    /// let invalid = invalid.unwrap();
    /// let invalid = cromulent::utf8::WordList::new_eager(&invalid);
    /// assert!(invalid.is_err());
    ///
    /// # Ok::<(), cromulent::WordError>(())
    pub fn new_eager(list: &'u WordListCStr<'l>) -> Result<Self, std::str::Utf8Error> {
        list.into_iter()
            .fold(Ok(list), |original, element| element.to_str().and(original))
            .map(WordList)
    }

    /// Return `true` if no words were expanded.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default().expand("")?;
    /// let utf8 = cromulent::utf8::WordList::from(&words);
    /// assert!(utf8.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 utf8 = cromulent::utf8::WordList::from(&words);
    /// assert_eq!(utf8.len(), 0);
    ///
    /// # Ok::<(), cromulent::WordError>(())
    /// ```
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(seq 3)")?;
    /// let utf8 = cromulent::utf8::WordList::from(&words);
    /// assert_eq!(utf8.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("$(seq 3)")?;
    /// let utf8 = cromulent::utf8::WordList::from(&words);
    ///
    /// assert_eq!(utf8.get(1), Some(&utf8[1]));
    /// assert_eq!(utf8.get(3), 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()`.
    /// * ...the word at `index` is valid UTF-8.
    ///
    /// There is no API for foregoing only one of these checks.
    ///
    /// # Examples
    ///
    /// ```
    /// let words = cromulent::WordExpander::default()
    ///     .allow_commands()
    ///     .expand("$(seq 3)")?;
    /// let utf8 = words.utf8();
    ///
    /// assert_eq!(unsafe { utf8.get_unchecked(1) }, &utf8[1]);
    /// // DANGER!
    /// // utf8.get_unchecked(3);
    ///
    /// let invalid = cromulent::WordExpander::default()
    ///     .expand_cstr(&std::ffi::CString::new(vec![243, 222])?)?;
    /// let invalid = invalid.utf8();
    ///
    /// // DANGER!
    /// // invalid.get_unchecked(0);
    ///
    /// # 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);
        let bytes = element.to_bytes();

        std::str::from_utf8_unchecked(bytes)
    }
}

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

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

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

impl<'r, 'u: 'r, 'l: 'u> Iterator for Iter<'r, 'u, 'l> {
    type Item = &'r <WordList<'u, '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)
        }
    }
}