linurgy 0.4.0

Manipulate the output of multiple newlines. Replace/Insert/Append newlines with any string. Input and output from stdio/files/buffers.
Documentation
#![doc(html_root_url = "https://docs.rs/linurgy/0.4.0")]
//! `linurgy` provides an interface for manipulating multiple newlines in text.
//! Interaction with this library happens through
//! [`LinurgyBuilder`](struct.LinurgyBuilder.html).
//!
//! # Examples
//!
//! Read stdin and for each empty line, append an extra line to stdout.
//! ```rust
//! # use std::error::Error;
//! # use linurgy::LinurgyBuilder;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! LinurgyBuilder::new()
//!     .set_newline_trigger(2)
//!     .set_new_text("\n")
//!     .run()?;
//! #
//! # Ok(())
//! # }
//! ```
//!
//! Read from one buffer, remove all empty lines, and output to another buffer.
//! ```rust
//! # use std::error::Error;
//! # use linurgy::{LinurgyBuilder, Input, Output, EditType};
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let input = String::from("Remove\n\nEvery\n\nEmpty\n\nLine\n");
//! let mut output = String::new();
//!
//! LinurgyBuilder::new()
//!     .set_input(Input::Buffer(&input))
//!     .set_output(Output::Buffer(&mut output))
//!     .set_newline_trigger(2)
//!     .set_edit_type(EditType::Replace)
//!     .set_new_text("\n")
//!     .run();
//!
//! assert_eq!("Remove\nEvery\nEmpty\nLine\n", &output);
//! #
//! # Ok(())
//! # }
//! ```

use std::fs;
use std::io::{self, Write};

/// Type of input stream to edit
#[derive(PartialEq, Debug)]
pub enum Input<'a> {
    /// Basic line by line read from stdin
    StdIn,

    /// Read from a given filename
    File(&'a str),

    /// Read from a string
    Buffer(&'a str),
}

/// Type of output stream to write edits to
#[derive(PartialEq, Debug)]
pub enum Output<'b> {
    /// Basic line by line output to stdout
    StdOut,

    /// Write to a given filename
    File(&'b str),

    /// Write to a given `String` buffer
    Buffer(&'b mut String),
}

/// Which action to implement when editing newlines
#[derive(PartialEq, Debug)]
pub enum EditType {
    /// New edits will appear after newlines
    Append,

    /// New edits will appear before newlines
    Insert,

    /// New edits will appear instead of newlines
    Replace,
}

/// Use this to prepare and execute linurgy editing on a stream.
///
/// A linurgy consists of an [`Input`](enum.Input.html), which will be read
/// line by line, edited by user defined rules, and then streamed into an
/// [`Output`](enum.Output.html).
#[derive(Debug)]
pub struct LinurgyBuilder<'a, 'b, 'c> {
    input: Input<'a>,
    output: Output<'b>,
    newline_count_trigger: u8,
    new_text: &'c str,
    edit_type: EditType,
    buffer: String,
    file: Option<fs::File>,
}

impl Default for LinurgyBuilder<'_, '_, '_> {
    fn default() -> Self {
        LinurgyBuilder {
            input: Input::StdIn,
            output: Output::StdOut,
            newline_count_trigger: 2,
            new_text: "-------\n",
            edit_type: EditType::Append,
            buffer: String::new(),
            file: None,
        }
    }
}

impl<'a, 'b, 'c> LinurgyBuilder<'a, 'b, 'c> {
    /// Instantiate a new builder with default values.
    /// - Input: [`Input::StdIn`](enum.Input.html#variant.StdIn),
    /// - Output: [`Output::StdOut`](enum.Output.html#variant.StdOut),
    /// - Newline count trigger: 2,
    /// - New text : "-------\n",
    /// - EditType: [`EditType::Append`](enum.EditType.html#variant.Append)
    ///
    /// This will read from `stdin`,
    /// add dashes after 2 empty lines, and write to `stdout`.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the input source to read text from.
    ///
    /// # Examples
    /// Using an in-memory [`Buffer`](enum.Input.html#variant.Buffer)
    /// ```rust
    /// # use linurgy::{LinurgyBuilder, Input};
    /// let text = String::from("Sample text\n\n\n");
    /// let mut linurgy = LinurgyBuilder::new();
    ///
    /// linurgy.set_input(Input::Buffer(&text));
    /// ```
    /// Read from a [`File`](enum.Input.html#variant.File)
    /// ```rust
    /// # use linurgy::{LinurgyBuilder, Input};
    /// let mut linurgy = LinurgyBuilder::new();
    ///
    /// linurgy.set_input(Input::File("filename.txt"));
    /// ```
    pub fn set_input(&mut self, input: Input<'a>) -> &mut Self {
        self.input = input;
        self
    }

    /// Set the output stream to write to.
    ///
    /// # Examples
    /// Using an in-memory [`Buffer`](enum.Output.html#variant.Buffer)
    /// ```rust
    /// # use linurgy::{LinurgyBuilder, Output};
    /// let mut buffer = String::new();
    /// let mut linurgy = LinurgyBuilder::new();
    ///
    /// linurgy.set_output(Output::Buffer(&mut buffer));
    /// ```
    /// Write straight to a [`File`](enum.Output.html#variant.File)
    /// ```rust
    /// # use linurgy::{LinurgyBuilder, Output};
    /// let mut linurgy = LinurgyBuilder::new();
    ///
    /// linurgy.set_output(Output::File("filename.txt"));
    /// ```
    pub fn set_output(&mut self, output: Output<'b>) -> &mut Self {
        self.output = output;
        self
    }

    /// Set the newline count to trigger editing.
    ///
    /// # Example
    /// Add edit string after every 5 empty lines
    /// ```rust
    /// # use linurgy::{LinurgyBuilder};
    /// let mut linurgy = LinurgyBuilder::new();
    /// linurgy.set_newline_trigger(5);
    /// ```
    pub fn set_newline_trigger(&mut self, count: u8) -> &mut Self {
        self.newline_count_trigger = count;
        self
    }

    /// Set the text that will be used when the newline trigger is reached.
    ///
    /// # Example
    /// Add a line of dots after every empty line
    /// ```rust
    /// # use linurgy::{LinurgyBuilder};
    /// let mut linurgy = LinurgyBuilder::new();
    /// let new_text = format!("{}\n", ". ".repeat(25));
    ///
    /// linurgy.set_newline_trigger(1);
    /// linurgy.set_new_text(&new_text);
    /// ```
    pub fn set_new_text(&mut self, new_text: &'c str) -> &mut Self {
        self.new_text = new_text;
        self
    }

    /// Set how new text is added after the newline trigger is reached.
    ///
    /// # Example
    /// Replace double empty lines with a line of dashes
    /// ```rust
    /// # use linurgy::{LinurgyBuilder, EditType};
    /// let mut linurgy = LinurgyBuilder::new();
    /// linurgy.set_edit_type(EditType::Replace);
    /// ```
    pub fn set_edit_type(&mut self, edit_type: EditType) -> &mut Self {
        self.edit_type = edit_type;
        self
    }

    /// Execute the linurgy edits on the specified input stream.
    ///
    /// This function will block until the input stream is exhausted.
    /// If the input stream is [`Input::StdIn`](enum.Input.html#varient.StdIn),
    /// then `stdin` will be locked while this function runs.
    /// If `stdin` is locked elsewhere, this function will block until it
    /// becomes available again.
    ///
    /// # Examples
    /// Execute default behaviour and add dashes to
    /// double newlines from `stdin`
    /// ```rust
    /// # use std::error::Error;
    /// # use linurgy::LinurgyBuilder;
    /// # fn main() -> Result<(), Box<dyn Error>> {
    /// LinurgyBuilder::new().run()?;
    /// #
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// If this function encounters any form of I/O or other error, an error
    /// variant will be returned.
    pub fn run(&mut self) -> Result<(), io::Error> {
        if let Output::File(path) = &self.output {
            self.file = Some(fs::File::create(path)?)
        }

        match self.input {
            Input::StdIn => {
                let stdin = io::stdin();
                let reader = stdin.lock();
                self.process(reader)?;
            }
            Input::File(name) => {
                let file = fs::File::open(name)?;
                let reader = io::BufReader::new(file);
                self.process(reader)?;
            }
            Input::Buffer(buffer) => {
                let reader = io::Cursor::new(buffer);
                self.process(reader)?;
            }
        }

        Ok(())
    }

    fn add_newlines_to_buffer(&mut self, count: u8) {
        for _ in 0..count {
            self.buffer += "\n";
        }
    }

    fn process(&mut self, reader: impl io::BufRead) -> Result<(), io::Error> {
        let mut newlines = 0;
        let mut rollon = false;

        for line in reader.lines() {
            let line = line?;
            if !line.is_empty() && line.chars().any(|c| !c.is_whitespace()) {
                if rollon {
                    self.add_newlines_to_buffer(1);
                }
                newlines = 1;
                self.buffer += &line;
            } else {
                newlines += 1;
            }

            if newlines == self.newline_count_trigger {
                match self.edit_type {
                    EditType::Append => {
                        self.add_newlines_to_buffer(newlines);
                        self.buffer += self.new_text;
                    }
                    EditType::Insert => {
                        self.buffer += self.new_text;
                        self.add_newlines_to_buffer(newlines);
                    }
                    EditType::Replace => self.buffer += self.new_text,
                }
                rollon = false;
                newlines = 0;
            } else {
                rollon = true;
            }

            self.write()?;
        }

        if rollon {
            self.add_newlines_to_buffer(1);
            self.write()?;
        }

        Ok(())
    }

    fn write(&mut self) -> Result<(), io::Error> {
        match self.output {
            Output::StdOut => print!("{}", self.buffer),
            Output::File(_) => {
                if let Some(file) = &mut self.file {
                    file.write_all(self.buffer.as_bytes())?
                } else {
                    return Err(io::Error::from(io::ErrorKind::NotFound));
                }
            }
            Output::Buffer(ref mut buffer) => buffer.push_str(&self.buffer),
        }
        self.buffer.clear();

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    const EXPECT_MSG: &'static str = "should never error";

    #[test]
    fn default_linurgy_builder() {
        let lb = LinurgyBuilder::new();
        let default = LinurgyBuilder {
            input: Input::StdIn,
            output: Output::StdOut,
            newline_count_trigger: 2,
            new_text: "-------\n",
            edit_type: EditType::Append,
            buffer: String::new(),
            file: None,
        };
        assert_eq!(default.input, lb.input);
        assert_eq!(default.output, lb.output);
        assert_eq!(default.newline_count_trigger, lb.newline_count_trigger);
        assert_eq!(default.new_text, lb.new_text);
        assert_eq!(default.edit_type, lb.edit_type);
        assert_eq!(default.buffer, lb.buffer);
        assert_eq!(default.file.is_none(), lb.file.is_none());
    }

    #[test]
    fn linurgy_set_input() {
        let buffer = String::from("Test builder");
        let mut lb = LinurgyBuilder::new();

        lb.set_input(Input::Buffer(&buffer));
        match lb.input {
            Input::Buffer(text) => assert_eq!(&buffer, text),
            _ => panic!("Correct type not implemented"),
        }

        lb.set_input(Input::File("filename"));
        match lb.input {
            Input::File(text) => assert_eq!("filename", text),
            _ => panic!("Correct type not implemented"),
        }

        lb.set_input(Input::StdIn);
        assert_eq!(Input::StdIn, lb.input);
    }

    #[test]
    fn linurgy_set_output() {
        let mut buffer = String::from("Test builder");
        let mut buffer2 = String::from("Test builder");
        let mut lb = LinurgyBuilder::new();

        lb.set_output(Output::Buffer(&mut buffer));
        match lb.output {
            Output::Buffer(ref text) => assert_eq!(&&mut buffer2, text),
            _ => panic!("Correct type not implemented"),
        }

        lb.set_output(Output::File("filename"));
        match lb.output {
            Output::File(text) => assert_eq!("filename", text),
            _ => panic!("Correct type not implemented"),
        }

        lb.set_output(Output::StdOut);
        assert_eq!(Output::StdOut, lb.output);
    }

    #[test]
    fn linurgy_set_newline_trigger() {
        let mut lb = LinurgyBuilder::new();
        lb.set_newline_trigger(5);
        assert_eq!(5, lb.newline_count_trigger);
    }

    #[test]
    fn linurgy_set_new_text() {
        let mut lb = LinurgyBuilder::new();
        lb.set_new_text("cheese");
        assert_eq!("cheese", lb.new_text);
    }

    #[test]
    fn linurgy_set_edit_type() {
        let mut lb = LinurgyBuilder::new();
        lb.set_edit_type(EditType::Insert);
        assert_eq!(EditType::Insert, lb.edit_type);
    }

    fn get_testable_linurgy_builder<'a, 'b, 'c>(
        output: &'b mut String,
    ) -> LinurgyBuilder<'a, 'b, 'c> {
        output.clear();
        let mut lb = LinurgyBuilder::new();
        lb.set_output(Output::Buffer(output));
        lb
    }

    #[test]
    fn linurgy_write() {
        let mut output = String::new();
        let mut lb = get_testable_linurgy_builder(&mut output);

        lb.buffer = "testline\n".to_owned();
        lb.write().expect(EXPECT_MSG);
        assert_eq!("testline\n", output);

        let mut lb = get_testable_linurgy_builder(&mut output);

        lb.buffer = "testline\n".to_owned();
        lb.write().expect(EXPECT_MSG);
        lb.buffer = "testline\n".to_owned();
        lb.write().expect(EXPECT_MSG);
        assert_eq!("testline\ntestline\n", output);
    }

    #[test]
    fn linurgy_process() {
        fn assert_expected_output_from_input(expected: &str, input: &str) {
            let mut output = String::new();
            let mut lb = get_testable_linurgy_builder(&mut output);
            let input = String::from(input);
            let reader = io::Cursor::new(&input);
            lb.process(reader).expect(EXPECT_MSG);
            assert_eq!(expected, &output);
        }

        assert_expected_output_from_input("test\nlines\n", "test\nlines\n");
        assert_expected_output_from_input("\n\n-------\n", "\n\n");
        assert_expected_output_from_input(
            "\n\n-------\n test post text\n\n-------\n",
            "\n\n test post text\n\n",
        );
    }

    #[test]
    fn linurgy_run() {
        let input = String::from("test\nlines\n");
        let mut output = String::new();
        let mut lb = LinurgyBuilder::new();
        lb.set_input(Input::Buffer(&input));
        lb.set_output(Output::Buffer(&mut output));
        lb.run().expect(EXPECT_MSG);
        assert_eq!("test\nlines\n", &output);
    }
}