rust-csv icon indicating copy to clipboard operation
rust-csv copied to clipboard

How to refer to a csv::Writer without caring what it's writing to?

Open znewman01 opened this issue 5 years ago • 1 comments

What version of the csv crate are you using?

csv 1.1.3

Briefly describe the question, bug or feature request.

I'm trying to implement the common pattern of accepting - to indicate STDOUT/STDIN in CLI arguments representing paths, and running into trouble assigning either the result of a csv::Writer::from_writer or csv::Writer::from_path to the same variable.

Include a complete program demonstrating a problem.

Rust playground

Here's (approximately) what I want to write:

fn run(path: &str) -> Result<(), Box<dyn Error>> {
    let mut wtr = match path {
        "-" => Box::new(csv::Writer::from_writer(io::stdout())),
        path => Box::new(csv::Writer::from_path(path)?),
    };
    wtr.write_record(&["a", "b", "c"])?;
    wtr.flush()?;
    Ok(())
}

I've found an acceptable way to do it by adding some indirection:

fn run_generic_inner<W: Write>(mut wtr: csv::Writer<W>) -> Result<(), Box<dyn Error>> {
    wtr.write_record(&["a", "b", "c"])?;
    wtr.flush()?;
    Ok(())
}

fn run_generic(path: &str) -> Result<(), Box<dyn Error>> {
    match path {
        "-" => run_generic_inner(csv::Writer::from_writer(io::stdout())),
        path => run_generic_inner(csv::Writer::from_path(path)?),
    }
}

What is the observed behavior of the code above?

The former snippet fails to compile; the latter works correctly. The error:

error[E0308]: `match` arms have incompatible types
  --> src/main.rs:9:17
   |
7  |       let mut wtr = match path {
   |  ___________________-
8  | |         "-" => Box::new(csv::Writer::from_writer(io::stdout())),
   | |                ------------------------------------------------ this is found to be of type `std::boxed::Box<csv::writer::Writer<std::io::Stdout>>`
9  | |         path => Box::new(csv::Writer::from_path(path)?),
   | |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `std::io::Stdout`, found struct `std::fs::File`
10 | |     };
   | |_____- `match` arms have incompatible types
   |
   = note: expected type `std::boxed::Box<csv::writer::Writer<std::io::Stdout>>`
            found struct `std::boxed::Box<csv::writer::Writer<std::fs::File>>`

What is the expected or desired behavior of the code above?

I'd like to be able to pass around a reference to a csv::Writer without knowing/caring what it's writing to.

One way to do this would be to add a Writer trait or similar, and I could accept Box<Writer> etc. There might be something much more idiomatic.

(The code with the generic parameter is a little annoying for my use case, though workable.)

Perhaps even better, a cookbook example or even rust-csv support for the --means-STDIN/OUT pattern.

P.S.

Thanks for a great library!

znewman01 avatar Mar 26 '20 19:03 znewman01

Sure, great question! You have the write idea. The key is that csv::Writer is generic over anything that implements the io::Write trait. It turns out that Box<dyn io::Write> implements the io::Write trait, so you can just use that! For example:

use std::env;
use std::ffi::OsStr;
use std::fs::File;
use std::io;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = env::args_os().nth(1).unwrap_or_else(|| {
        OsStr::new("-").to_os_string()
    });
    let iowtr: Box<dyn io::Write> = if path.to_string_lossy() == "-" {
        Box::new(io::stdout())
    } else {
        Box::new(File::create(path)?)
    };

    let mut wtr = csv::Writer::from_writer(iowtr);
    wtr.write_record(&["foo", "bar", "quux"])?;
    wtr.flush()?;
    Ok(())
}

And yeah, this would be a great thing to add to the cookbook.

BurntSushi avatar Mar 26 '20 20:03 BurntSushi