diff --git a/Cargo.lock b/Cargo.lock index d728d15..8c8ebea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "ansi_term" version = "0.11.0" diff --git a/src/lib.rs b/src/lib.rs index 13f0612..d30e9bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,9 @@ //! //! It does not do anything more than that, which makes it so fast. +use std::error::Error; +use std::io::{BufReader, BufWriter, Read, Write}; + /// /// Set the indentation used for the formatting. /// @@ -24,32 +27,52 @@ pub enum Indentation<'a> { /// The default indentation is faster than a custom one /// pub fn format_json(json: &str, indentation: Indentation) -> String { - // at least as big as the input to avoid resizing - // this might be too big if the input string is formatted in a weird way, but that's not expected, and it will still be efficient - let mut out = String::with_capacity(json.len()); + let mut reader = BufReader::new(json.as_bytes()); + let mut writer = BufWriter::new(Vec::new()); + format_json_buffered(&mut reader, &mut writer, indentation).unwrap(); + String::from_utf8(writer.into_inner().unwrap()).unwrap() +} + +/// +/// # Formats a json string +/// +/// The indentation can be set to any value using [Indentation](jsonformat::Indentation) +/// The default value is two spaces +/// The default indentation is faster than a custom one +/// +pub fn format_json_buffered( + reader: &mut BufReader, + writer: &mut BufWriter, + indentation: Indentation, +) -> Result<(), Box> +where + R: Read, + W: Write, +{ let mut escaped = false; let mut in_string = false; let mut indent_level = 0usize; let mut newline_requested = false; // invalidated if next character is ] or } - for char in json.chars() { + for char in reader.bytes() { + let char = char?; if in_string { let mut escape_here = false; match char { - '"' => { + b'"' => { if !escaped { in_string = false; } } - '\\' => { + b'\\' => { if !escaped { escape_here = true; } } _ => {} } - out.push(char); + writer.write_all(&[char])?; escaped = escape_here; } else { let mut auto_push = true; @@ -57,65 +80,73 @@ pub fn format_json(json: &str, indentation: Indentation) -> String { let old_level = indent_level; match char { - '"' => in_string = true, - ' ' | '\n' | '\t' => continue, - '[' => { + b'"' => in_string = true, + b' ' | b'\n' | b'\t' => continue, + b'[' => { indent_level += 1; request_newline = true; } - '{' => { + b'{' => { indent_level += 1; request_newline = true; } - '}' | ']' => { + b'}' | b']' => { indent_level = indent_level.saturating_sub(1); if !newline_requested { // see comment below about newline_requested - out.push('\n'); - indent(&mut out, indent_level, indentation); + writer.write_all(&[b'\n'])?; + indent_buffered(writer, indent_level, indentation)?; } } - ':' => { + b':' => { auto_push = false; - out.push(char); - out.push(' '); + writer.write_all(&[char])?; + writer.write_all(&[b' '])?; } - ',' => { + b',' => { request_newline = true; } _ => {} } - if newline_requested && char != ']' && char != '}' { + if newline_requested && char != b']' && char != b'}' { // newline only happens after { [ and , // this means we can safely assume that it being followed up by } or ] // means an empty object/array - out.push('\n'); - indent(&mut out, old_level, indentation); + writer.write_all(&[b'\n'])?; + indent_buffered(writer, old_level, indentation)?; } if auto_push { - out.push(char); + writer.write_all(&[char])?; } newline_requested = request_newline; } } - out + Ok(()) } -fn indent(buf: &mut String, level: usize, indent_str: Indentation) { +fn indent_buffered( + writer: &mut BufWriter, + level: usize, + indent_str: Indentation, +) -> Result<(), Box> +where + W: std::io::Write, +{ for _ in 0..level { match indent_str { Indentation::Default => { - buf.push(' '); - buf.push(' '); + writer.write_all(b" ")?; } Indentation::Custom(indent) => { - buf.push_str(indent); + writer.write_all(indent.as_bytes())?; } } } + + Ok(()) } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index 238950f..778395c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ use clap::clap_app; -use jsonformat::{format_json, Indentation}; -use std::fs; -use std::io; -use std::io::Read; +use jsonformat::{format_json_buffered, Indentation}; +use std::error::Error; +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Write}; -fn main() -> Result<(), io::Error> { +fn main() -> Result<(), Box> { let matches = clap_app!(jsonformat => (version: "1.1") (author: "nilstrieb ") @@ -16,13 +16,16 @@ fn main() -> Result<(), io::Error> { ) .get_matches(); - let str = match matches.value_of("input") { - Some(path) => fs::read_to_string(path)?, + // Note: on-stack dynamic dispatch + let (mut file, mut stdin); + let reader: &mut dyn Read = match matches.value_of("input") { + Some(path) => { + file = File::open(path)?; + &mut file + } None => { - let mut buf = String::new(); - let stdin = std::io::stdin(); - stdin.lock().read_to_string(&mut buf)?; - buf + stdin = std::io::stdin(); + &mut stdin } }; @@ -41,8 +44,6 @@ fn main() -> Result<(), io::Error> { None => Indentation::Default, }; - let formatted = format_json(&str, indent); - let mut output = matches.value_of("output"); let mut windows_output_default_file: Option = None; @@ -57,12 +58,22 @@ fn main() -> Result<(), io::Error> { output = windows_output_default_file.as_deref().or(output); - match output { - Some(file) => { - fs::write(file, formatted)?; - } - None => println!("{}", formatted), - } + // Note: on-stack dynamic dispatch + let (mut file, mut stdout); + let writer: &mut dyn Write = match output { + Some(filename) => { + file = File::create(filename)?; + &mut file + }, + None => { + stdout = std::io::stdout(); + &mut stdout + }, + }; + + let mut reader = BufReader::new(reader); + let mut writer = BufWriter::new(writer); + format_json_buffered(&mut reader, &mut writer, indent)?; Ok(()) }