Skip to content

Commit 61e67f5

Browse files
committed
Make it so.
0 parents commit 61e67f5

File tree

5 files changed

+165
-0
lines changed

5 files changed

+165
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/target/
2+
**/*.rs.bk
3+
Cargo.lock
4+
.idea/

Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "csver"
3+
version = "0.1.0"
4+
license = "MIT OR Apache-2.0"
5+
authors = ["Daniel Gregoire <daniel.l.gregoire@gmail.com>"]
6+
7+
[lib]
8+
name="csver"
9+
path="src/lib.rs"
10+
11+
[[bin]]
12+
name="csver"
13+
path="src/main.rs"
14+
15+
[dependencies]
16+
clap = "~2.26"
17+
csv = "1.0.0-beta.4"
18+
serde_json = "~1.0"

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# CSVer
2+
3+
CSV utilities at the CLI.
4+
5+
- Convert JSON array over STDIN to CSV over STDOUT
6+
- Specify CSV field/column delimiter
7+
8+
## License
9+
10+
Copyright © 2017 Daniel Gregoire.
11+
12+
Licensed under either of
13+
14+
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
15+
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
16+
17+
at your option.
18+
19+
### Contribution
20+
21+
Unless you explicitly state otherwise, any contribution intentionally submitted
22+
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
23+
additional terms or conditions.

src/lib.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
extern crate csv;
2+
extern crate serde_json;
3+
4+
use std::io;
5+
use std::process;
6+
use serde_json::value::Value;
7+
8+
pub fn json_to_csv(stdin: &str, csv_delimiter: u8) -> io::Result<()> {
9+
let mut is_array_of_objects = false;
10+
let mut wtr = csv::WriterBuilder::new()
11+
.delimiter(csv_delimiter)
12+
.from_writer(io::stdout());
13+
if let Ok(json) = serde_json::from_str::<Value>(stdin) {
14+
if let Some(entries) = json.as_array() {
15+
if let Some(example) = entries.first() {
16+
if let Some(example_object) = example.as_object() {
17+
let ks = example_object.keys();
18+
// WRITE THE HEADER using example
19+
wtr.write_record(ks)?;
20+
wtr.flush()?;
21+
is_array_of_objects = true;
22+
} else {
23+
eprintln!("Expected a JSON array of objects. Got: {}", json);
24+
process::exit(1);
25+
}
26+
} else {
27+
eprintln!("Will not write empty CSV, got empty JSON array.");
28+
process::exit(1);
29+
}
30+
31+
// RECORDS
32+
if is_array_of_objects {
33+
for entry in entries {
34+
let o = entry.as_object().unwrap();
35+
let vals = o.values().map(|x: &Value| match *x {
36+
Value::Null => "null".to_owned(),
37+
Value::Bool(ref b) => format!("{}", b),
38+
Value::Number(ref n) => format!("{}", n),
39+
Value::String(ref s) => s.to_owned(),
40+
Value::Array(ref a) => {
41+
eprintln!("Nested arrays not supported. Found {:?}", a);
42+
process::exit(1);
43+
}
44+
Value::Object(ref o) => {
45+
eprintln!("Nested objects not supported. Found {:?}", o);
46+
process::exit(1);
47+
}
48+
});
49+
wtr.write_record(vals)?;
50+
}
51+
}
52+
} else {
53+
eprintln!("Expected a JSON array of objects. Got: {}", json);
54+
process::exit(1);
55+
}
56+
} else {
57+
eprintln!("Non-JSON found: {}", stdin);
58+
process::exit(1);
59+
}
60+
Ok(())
61+
}
62+
63+
#[cfg(test)]
64+
mod tests {
65+
#[test]
66+
fn it_works() {}
67+
}

src/main.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
extern crate clap;
2+
extern crate csver;
3+
4+
use clap::{App, Arg};
5+
use std::io::{self, Read};
6+
use std::process;
7+
8+
fn main() {
9+
let matches = App::new("csver")
10+
.version("0.1.0")
11+
.author("Daniel Gregoire <daniel.l.gregoire@gmail.com>")
12+
.about("JSON array over STDIN --> CSV over STDOUT")
13+
.arg(
14+
Arg::with_name("delimiter")
15+
.value_name("DELIMITER")
16+
.help("CSV delimiter to use between fields in each entry.")
17+
.default_value("comma")
18+
.long("delimiter")
19+
.short("d")
20+
.possible_values(&["comma", "tab"])
21+
.takes_value(true),
22+
)
23+
.get_matches();
24+
25+
let delimiter_arg = matches.value_of("delimiter").unwrap().to_lowercase();
26+
let delimiter = match delimiter_arg.as_ref() {
27+
"comma" => b",",
28+
"tab" => b"\t",
29+
_ => {
30+
eprintln!("Unsupported CSV delimiter. Only 'comma' or 'tab' allowed.");
31+
process::exit(1);
32+
}
33+
};
34+
println!(
35+
"Delimiter arg was {} and final is {:?}",
36+
delimiter_arg,
37+
delimiter
38+
);
39+
40+
let stdin = io::stdin();
41+
let mut buffer = String::new();
42+
stdin.lock().read_to_string(&mut buffer).expect(
43+
"Couldn't read from STDIN",
44+
);
45+
// TODO Figure out case when nothing comes in over stdin. Tools like grep just hang.
46+
if buffer.is_empty() {
47+
println!("{}", matches.usage());
48+
process::exit(1);
49+
} else if let Err(e) = csver::json_to_csv(&buffer, delimiter[0]) {
50+
eprintln!("Error converting JSON array to CSV: {:?}", e);
51+
process::exit(1);
52+
}
53+
}

0 commit comments

Comments
 (0)