rew 0.3.0

A text processing CLI tool that rewrites FS paths according to a pattern.
Documentation
use crate::input::{Splitter, Terminator};
use crate::symbols::{DIFF_IN, DIFF_OUT};
use std::fmt;
use std::io::{BufRead, Error, ErrorKind, Result};
use std::path::PathBuf;

struct Position {
    item: usize,
    offset: usize,
}

impl Position {
    pub fn new() -> Self {
        Self { item: 1, offset: 0 }
    }

    pub fn increment(&mut self, size: usize) {
        self.item += 1;
        self.offset += size;
    }
}

impl fmt::Display for Position {
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "item #{} at offset {}", self.item, self.offset)
    }
}

pub struct PathDiff<I: BufRead> {
    splitter: Splitter<I>,
    position: Position,
}

impl<I: BufRead> PathDiff<I> {
    pub fn new(input: I, terminator: Terminator) -> Self {
        Self {
            splitter: Splitter::new(input, terminator),
            position: Position::new(),
        }
    }

    pub fn read(&mut self) -> Result<Option<(PathBuf, PathBuf)>> {
        let (in_path, in_size) = match self.splitter.read()? {
            Some((value, size)) => (extract_path(value, &self.position, DIFF_IN)?, size),
            None => return Ok(None),
        };
        self.position.increment(in_size);

        let (out_path, out_size) = match self.splitter.read()? {
            Some((value, size)) => (extract_path(value, &self.position, DIFF_OUT)?, size),
            None => return Err(make_unexpected_eof_error(&self.position, DIFF_OUT)),
        };
        self.position.increment(out_size);

        Ok(Some((in_path, out_path)))
    }
}

fn extract_path(value: &str, position: &Position, prefix: char) -> Result<PathBuf> {
    if let Some(first_char) = value.chars().next() {
        if first_char == prefix {
            let path = &value[prefix.len_utf8()..];
            if path.is_empty() {
                Err(Error::new(
                    ErrorKind::UnexpectedEof,
                    format!("Expected a path after '{}' ({})", prefix, position),
                ))
            } else {
                Ok(path.into())
            }
        } else {
            Err(Error::new(
                ErrorKind::InvalidData,
                format!(
                    "Expected '{}' but got '{}' ({})",
                    prefix, first_char, position
                ),
            ))
        }
    } else {
        Err(make_unexpected_eof_error(position, prefix))
    }
}

fn make_unexpected_eof_error(position: &Position, prefix: char) -> Error {
    Error::new(
        ErrorKind::UnexpectedEof,
        format!("Expected '{}' ({})", prefix, position),
    )
}

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

    #[test]
    fn position() {
        let mut position = Position::new();
        assert_eq!(position.to_string(), "item #1 at offset 0");
        position.increment(1);
        assert_eq!(position.to_string(), "item #2 at offset 1");
        position.increment(2);
        assert_eq!(position.to_string(), "item #3 at offset 3");
    }

    mod path_diff {
        use super::*;
        use crate::testing::unpack_io_error;
        use test_case::test_case;

        #[test_case("",                       0, None                 ; "empty")]
        #[test_case("<abc\n>def\n< g \n> h ", 0, Some(("abc", "def")) ; "nonempty 0")]
        #[test_case("<abc\n>def\n< g \n> h ", 1, Some((" g ", " h ")) ; "nonempty 1")]
        #[test_case("<abc\n>def\n< g \n> h ", 2, None                 ; "nonempty 2")]
        fn ok(input: &str, position: usize, result: Option<(&str, &str)>) {
            let mut path_diff =
                PathDiff::new(input.as_bytes(), Terminator::Newline { required: false });

            for _ in 0..position {
                path_diff.read().unwrap_or_default();
            }

            assert_eq!(
                path_diff.read().map_err(unpack_io_error),
                Ok(result.map(|(first, second)| (first.into(), second.into())))
            );
        }

        type E = ErrorKind;

        #[test_case("a",     E::InvalidData,   "Expected '<' but got 'a' (item #1 at offset 0)"  ; "in prefix invalid")]
        #[test_case("<",     E::UnexpectedEof, "Expected a path after '<' (item #1 at offset 0)" ; "in path missing")]
        #[test_case("<a",    E::UnexpectedEof, "Expected '>' (item #2 at offset 2)"              ; "in terminator missing")]
        #[test_case("<a\n",  E::UnexpectedEof, "Expected '>' (item #2 at offset 3)"              ; "out prefix missing")]
        #[test_case("<a\nb", E::InvalidData,   "Expected '>' but got 'b' (item #2 at offset 3)"  ; "out prefix invalid")]
        #[test_case("<a\n>", E::UnexpectedEof, "Expected a path after '>' (item #2 at offset 3)" ; "out path missing")]
        fn err(input: &str, kind: ErrorKind, message: &str) {
            assert_eq!(
                PathDiff::new(input.as_bytes(), Terminator::Newline { required: false })
                    .read()
                    .map_err(unpack_io_error),
                Err((kind, message.into()))
            )
        }
    }
}