use crate::{Error, Target};
use failure::ResultExt;
use serde::Serialize;
use serde_json::to_vec as write_json;
use std::io::Write;
use std::path::Path;
use std::sync::Arc;
pub fn stdout() -> Result<Target, Error> {
use std::io::{stdout, BufWriter};
let formatter = Formatter::init_with(|| Ok(BufWriter::new(stdout())))?;
Ok(Target::json(formatter))
}
pub fn file<T: AsRef<Path>>(name: T) -> Result<Target, Error> {
let path = name.as_ref().to_path_buf();
let formatter = Formatter::init_with(move || {
use std::fs::{File, OpenOptions};
use std::io::BufWriter;
let target = if path.exists() {
let mut f = OpenOptions::new()
.write(true)
.append(true)
.open(&path)
.with_context(|_| format!("Can't open file `{}` as JSON target", path.display()))?;
f.write_all(b"\n")?;
f
} else {
File::create(&path)?
};
Ok(BufWriter::new(target))
})?;
Ok(Target::json(formatter))
}
pub use self::test_helper::test;
#[derive(Clone)]
pub struct Formatter {
inner: Arc<InternalFormatter>,
}
impl Formatter {
pub(crate) fn init_with<W: Write, F: FnOnce() -> Result<W, Error> + Send + 'static>(
init: F,
) -> Result<Self, Error> {
Ok(Formatter {
inner: Arc::new(InternalFormatter::init_with(init)?),
})
}
pub fn write<T: Serialize>(&self, item: &T) -> Result<(), Error> {
self.send(Message::Write(write_json(item)?))?;
Ok(())
}
pub fn flush(&self) -> Result<(), Error> {
self.send(Message::Flush)?;
match self.inner.receiver.recv() {
Ok(Response::Flushed) => Ok(()),
msg => Err(Error::worker_error(format!("unexpected message {:?}", msg))),
}
}
pub(crate) fn write_separator(&mut self) -> Result<(), Error> {
self.send(Message::Write(vec![b'\n']))?;
Ok(())
}
fn send(&self, msg: Message) -> Result<(), Error> {
self.inner.sender.send(msg)?;
Ok(())
}
}
use crossbeam_channel as channel;
use std::thread;
struct InternalFormatter {
sender: channel::Sender<Message>,
receiver: channel::Receiver<Response>,
worker: Option<thread::JoinHandle<()>>,
}
impl InternalFormatter {
pub(crate) fn init_with<W: Write, F: FnOnce() -> Result<W, Error> + Send + 'static>(
init: F,
) -> Result<Self, Error> {
let (message_sender, message_receiver) = channel::unbounded();
let (response_sender, response_receiver) = channel::bounded(0);
let worker = thread::spawn(move || {
let mut buffer = match init() {
Ok(buf) => {
let _ = response_sender.send(Response::StartedSuccessfully);
buf
}
Err(e) => {
let _ = response_sender.send(Response::Error(e));
return;
}
};
macro_rules! maybe_log_error {
() => {
|e| {
if cfg!(debug_assertions) {
eprintln!("{}", e)
} else {
()
}
}
};
}
loop {
match message_receiver.recv() {
Ok(Message::Write(data)) => {
let _ = buffer.write_all(&data).map_err(maybe_log_error!());
}
Ok(Message::Flush) => {
let _ = buffer.flush().map_err(maybe_log_error!());
let _ = response_sender.send(Response::Flushed);
}
Ok(Message::Exit) | Err(_) => {
break;
}
};
}
});
match response_receiver.recv() {
Ok(Response::Error(error)) => Err(error),
Ok(Response::StartedSuccessfully) => Ok(InternalFormatter {
worker: Some(worker),
sender: message_sender,
receiver: response_receiver,
}),
msg => Err(Error::worker_error(format!("unexpected message {:?}", msg))),
}
}
}
impl Drop for InternalFormatter {
fn drop(&mut self) {
let _ = self.sender.send(Message::Exit);
if let Some(worker) = self.worker.take() {
let _ = worker.join();
}
}
}
#[derive(Debug)]
enum Message {
Write(Vec<u8>),
Flush,
Exit,
}
#[derive(Debug)]
enum Response {
StartedSuccessfully,
Error(Error),
Flushed,
}
#[macro_export]
macro_rules! render_json {
() => {
fn render_json(&self, fmt: &mut $crate::json::Formatter) -> Result<(), $crate::Error> {
fmt.write(self)?;
Ok(())
}
}
}
mod test_helper {
use super::Formatter;
use crate::test_buffer::TestBuffer;
use crate::Target;
use termcolor::Buffer;
pub fn test() -> TestTarget {
TestTarget {
buffer: Buffer::no_color().into(),
}
}
pub struct TestTarget {
buffer: TestBuffer,
}
impl TestTarget {
pub fn formatter(&self) -> Formatter {
let buffer = self.buffer.clone();
Formatter::init_with(|| Ok(buffer)).unwrap()
}
pub fn target(&self) -> Target {
Target::json(self.formatter())
}
pub fn to_string(&self) -> String {
let target = self.buffer.0.clone();
let buffer = target.read().unwrap();
String::from_utf8_lossy(buffer.as_slice()).to_string()
}
}
}
#[cfg(test)]
mod tests {
use crate::json;
use assert_fs::prelude::*;
use assert_fs::TempDir;
use predicates::prelude::*;
type Res = Result<(), ::failure::Error>;
#[test]
fn creates_a_new_file() -> Res {
let dir = TempDir::new()?;
let log_file = dir.child("log.json");
log_file.assert(predicate::path::missing());
{
let _target = json::file(log_file.path())?;
}
log_file.assert(predicate::path::exists());
Ok(())
}
#[test]
fn doesnt_truncate_existing_file() -> Res {
let dir = TempDir::new()?;
let log_file = dir.child("log.json");
let intitial_content = "{\"success\":true}\n";
log_file.write_str(intitial_content)?;
log_file.assert(predicate::path::exists());
{
let _target = json::file(log_file.path())?;
}
log_file.assert(
predicate::str::contains(intitial_content)
.from_utf8()
.from_file_path(),
);
Ok(())
}
#[test]
fn appends_to_existing_file() -> Res {
let dir = TempDir::new()?;
let log_file = dir.child("log.json");
let intitial_content = "{\"success\":true}\n";
log_file.write_str(intitial_content)?;
log_file.assert(predicate::path::exists());
let target = json::file(log_file.path())?;
let output = crate::new().add_target(target)?;
output.print("wtf")?;
output.flush()?;
log_file.assert(
predicate::str::ends_with("\"wtf\"")
.trim()
.from_utf8()
.from_file_path(),
);
Ok(())
}
#[test]
fn appends_newline_to_existing_file() -> Res {
let dir = TempDir::new()?;
let log_file = dir.child("log.json");
let intitial_content = "{\"success\":true}";
log_file.write_str(intitial_content)?;
log_file.assert(predicate::path::exists());
{
let _target = json::file(log_file.path())?;
}
log_file.assert(predicate::str::ends_with("\n").from_utf8().from_file_path());
Ok(())
}
}