graphitepdf-utils 0.4.0

Lightweight utility functions for GraphitePDF and related Rust workflows.
Documentation
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OneOrMany<T> {
    One(T),
    Many(Vec<T>),
}

impl<T> From<T> for OneOrMany<T> {
    fn from(value: T) -> Self {
        Self::One(value)
    }
}

impl<T> From<Vec<T>> for OneOrMany<T> {
    fn from(value: Vec<T>) -> Self {
        Self::Many(value)
    }
}

pub fn cast_array<T>(input: impl Into<OneOrMany<T>>) -> Vec<T> {
    match input.into() {
        OneOrMany::One(value) => vec![value],
        OneOrMany::Many(values) => values,
    }
}

pub fn adjust<T, F>(index: isize, mut adjuster: F, values: &[T]) -> Vec<T>
where
    T: Clone,
    F: FnMut(T) -> T,
{
    let mut result = values.to_vec();
    let len = result.len();

    if let Some(index) = normalize_index(index, len) {
        result[index] = adjuster(result[index].clone());
    }

    result
}

pub trait DropLast {
    type Output;

    fn drop_last(self) -> Self::Output;
}

pub fn drop_last<T>(value: T) -> T::Output
where
    T: DropLast,
{
    value.drop_last()
}

impl<T: Clone> DropLast for &[T] {
    type Output = Vec<T>;

    fn drop_last(self) -> Self::Output {
        self[..self.len().saturating_sub(1)].to_vec()
    }
}

impl<T> DropLast for Vec<T> {
    type Output = Vec<T>;

    fn drop_last(mut self) -> Self::Output {
        let _ = self.pop();
        self
    }
}

impl DropLast for &str {
    type Output = String;

    fn drop_last(self) -> Self::Output {
        let mut result = self.to_string();
        let _ = result.pop();
        result
    }
}

impl DropLast for String {
    type Output = String;

    fn drop_last(mut self) -> Self::Output {
        let _ = self.pop();
        self
    }
}

pub trait Last {
    type Output;

    fn last_value(self) -> Option<Self::Output>;
}

pub fn last<T>(value: T) -> Option<T::Output>
where
    T: Last,
{
    value.last_value()
}

impl<T: Clone> Last for &[T] {
    type Output = T;

    fn last_value(self) -> Option<Self::Output> {
        self.last().cloned()
    }
}

impl<T> Last for Vec<T> {
    type Output = T;

    fn last_value(self) -> Option<Self::Output> {
        self.into_iter().last()
    }
}

impl Last for &str {
    type Output = char;

    fn last_value(self) -> Option<Self::Output> {
        self.chars().next_back()
    }
}

impl Last for String {
    type Output = char;

    fn last_value(self) -> Option<Self::Output> {
        self.chars().next_back()
    }
}

pub fn repeat<T>(value: T, count: usize) -> Vec<T>
where
    T: Clone,
{
    vec![value; count]
}

pub trait Reverse {
    type Output;

    fn reverse_value(self) -> Self::Output;
}

pub fn reverse<T>(value: T) -> T::Output
where
    T: Reverse,
{
    value.reverse_value()
}

impl<T: Clone> Reverse for &[T] {
    type Output = Vec<T>;

    fn reverse_value(self) -> Self::Output {
        self.iter().cloned().rev().collect()
    }
}

impl<T> Reverse for Vec<T> {
    type Output = Vec<T>;

    fn reverse_value(mut self) -> Self::Output {
        self.reverse();
        self
    }
}

pub fn without<T>(excluded: &[T], values: &[T]) -> Vec<T>
where
    T: Clone + PartialEq,
{
    values
        .iter()
        .filter(|value| !excluded.iter().any(|excluded| excluded == *value))
        .cloned()
        .collect()
}

fn normalize_index(index: isize, len: usize) -> Option<usize> {
    if len == 0 {
        return None;
    }

    if index >= 0 {
        let index = usize::try_from(index).ok()?;
        return (index < len).then_some(index);
    }

    let len = isize::try_from(len).ok()?;
    let adjusted = len.checked_add(index)?;

    if adjusted < 0 {
        None
    } else {
        usize::try_from(adjusted).ok()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn casts_single_values_and_vectors() {
        assert_eq!(cast_array("foo"), vec!["foo"]);
        assert_eq!(cast_array::<&str>(vec!["foo"]), vec!["foo"]);
    }

    #[test]
    fn adjusts_values_using_positive_and_negative_indexes() {
        assert_eq!(adjust(1, |value| value * 2, &[1, 2, 3]), vec![1, 4, 3]);
        assert_eq!(adjust(-1, |value| value + 10, &[1, 2, 3]), vec![1, 2, 13]);
    }

    #[test]
    fn drops_last_for_slices_and_strings() {
        assert_eq!(drop_last(&[1, 2, 3][..]), vec![1, 2]);
        assert_eq!(drop_last("hello"), "hell");
    }

    #[test]
    fn gets_last_for_slices_and_strings() {
        assert_eq!(last(&[1, 2, 3][..]), Some(3));
        assert_eq!(last("abc"), Some('c'));
        assert_eq!(last(&[] as &[i32]), None);
    }

    #[test]
    fn repeats_reverses_and_filters_values() {
        assert_eq!(repeat("a", 3), vec!["a", "a", "a"]);
        assert_eq!(reverse(&[1, 2, 3][..]), vec![3, 2, 1]);
        assert_eq!(without(&[2, 4], &[1, 2, 3, 4, 5]), vec![1, 3, 5]);
    }
}