mod query_parser;
use std::path::PathBuf;
use std::str;
use std::{fs, process::exit};
use failure::{Error, Fail};
use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer};
use structopt::StructOpt;
use toml_edit::{value, Document, Item, Table, Value};
use query_parser::{parse_query, Query, TpathSegment};
#[derive(StructOpt)]
#[structopt(about)]
enum Args {
#[structopt(verbatim_doc_comment)]
Get {
#[structopt(parse(from_os_str))]
path: PathBuf,
query: String,
#[structopt(flatten)]
opts: GetOpts,
},
Set {
#[structopt(parse(from_os_str))]
path: PathBuf,
query: String,
value_str: String, },
}
#[derive(StructOpt)]
struct GetOpts {
#[structopt(long)]
output_toml: bool,
#[structopt(long, short)]
raw: bool,
}
#[derive(Debug, Fail)]
enum CliError {
#[fail(display = "syntax error in query: {}", _0)]
QuerySyntaxError(String),
#[fail(display = "numeric index into non-array")]
NotArray(),
#[fail(display = "array index out of bounds")]
ArrayIndexOob(),
}
#[derive(Debug, Fail)]
enum SilentError {
#[fail(display = "key not found: {}", key)]
KeyNotFound { key: String },
}
fn main() {
let args = Args::from_args();
let result = match args {
Args::Get { path, query, opts } => get(&path, &query, &opts),
Args::Set {
path,
query,
value_str,
} => set(&path, &query, &value_str),
};
result.unwrap_or_else(|err| {
match err.downcast::<SilentError>() {
Ok(_) => {}
Err(err) => {
eprintln!("toml: {}", err);
}
}
exit(1);
})
}
fn read_parse(path: &PathBuf) -> Result<Document, Error> {
let data = fs::read(path)?;
let data = str::from_utf8(&data)?;
Ok(data.parse::<Document>()?)
}
fn get(path: &PathBuf, query: &str, opts: &GetOpts) -> Result<(), Error> {
let tpath = parse_query_cli(query)?.0;
let doc = read_parse(path)?;
if opts.output_toml {
print_toml_fragment(&doc, &tpath);
return Ok(());
}
let item = walk_tpath(doc.as_item(), &tpath);
let item = item.ok_or(SilentError::KeyNotFound { key: query.into() })?;
if opts.raw {
if let Item::Value(Value::String(s)) = item {
println!("{}", s.value());
return Ok(());
}
}
println!("{}", serde_json::to_string(&JsonItem(item))?);
Ok(())
}
fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {
use TpathSegment::{Name, Num};
let mut item = doc.as_item();
let mut breadcrumbs = vec![];
for seg in tpath {
breadcrumbs.push((item, seg));
match seg {
Name(n) => item = &item[n],
Num(n) => item = &item[n],
}
}
let mut item = item.clone();
while let Some((parent, seg)) = breadcrumbs.pop() {
match (seg, parent) {
(Name(n), Item::Table(t)) => {
let mut next = t.clone();
while !next.is_empty() {
let (k, _) = next.iter().next().unwrap();
let k = String::from(k);
next.remove(&k);
}
next[n] = item;
item = Item::Table(next);
}
(Num(_), Item::ArrayOfTables(a)) => {
let mut next = a.clone();
next.clear();
match item {
#[rustfmt::skip]
Item::Table(t) => { next.push(t); }
_ => panic!("malformed TOML parse-tree"),
}
item = Item::ArrayOfTables(next);
}
_ => panic!("UNIMPLEMENTED: --output-toml inside inline data"), }
}
let doc = Document::from(item.into_table().unwrap());
print!("{}", doc);
}
fn set(path: &PathBuf, query: &str, value_str: &str) -> Result<(), Error> {
let tpath = parse_query_cli(query)?.0;
let mut doc = read_parse(path)?;
let mut item = doc.as_item_mut();
let mut already_inline = false;
let mut tpath = &tpath[..];
use TpathSegment::{Name, Num};
while let Some(seg) = tpath.first() {
tpath = &tpath[1..]; match seg {
Num(n) => {
let len = match &item {
Item::ArrayOfTables(a) => a.len(),
Item::Value(Value::Array(a)) => a.len(),
_ => Err(CliError::NotArray())?,
};
if n >= &len {
Err(CliError::ArrayIndexOob())?;
}
#[allow(clippy::single_match)]
match &item {
Item::Value(_) => already_inline = true,
_ => (),
};
item = &mut item[n];
}
Name(n) => {
match &item {
Item::Table(_) => (),
Item::Value(Value::InlineTable(_)) => already_inline = true,
_ => {
*item = if already_inline {
Item::Value(Value::InlineTable(Default::default()))
} else {
Item::Table(Table::new())
}
}
};
item = &mut item[n];
}
}
}
*item = value(value_str);
print!("{}", doc);
Ok(())
}
fn parse_query_cli(query: &str) -> Result<Query, CliError> {
parse_query(query).map_err(|_err| {
CliError::QuerySyntaxError(query.into()) })
}
fn walk_tpath<'a>(
mut item: &'a toml_edit::Item,
tpath: &[TpathSegment],
) -> Option<&'a toml_edit::Item> {
use TpathSegment::{Name, Num};
for seg in tpath {
match seg {
Name(n) => item = item.get(n)?,
Num(n) => item = item.get(n)?,
}
}
Some(item)
}
struct JsonItem<'a>(&'a toml_edit::Item);
impl Serialize for JsonItem<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self.0 {
Item::Value(v) => JsonValue(v).serialize(serializer),
Item::Table(t) => JsonTable(t).serialize(serializer),
Item::ArrayOfTables(a) => {
let mut seq = serializer.serialize_seq(Some(a.len()))?;
for t in a.iter() {
seq.serialize_element(&JsonTable(t))?;
}
seq.end()
}
Item::None => serializer.serialize_none(),
}
}
}
struct JsonTable<'a>(&'a toml_edit::Table);
impl Serialize for JsonTable<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (k, v) in self.0.iter() {
map.serialize_entry(k, &JsonItem(v))?;
}
map.end()
}
}
struct JsonValue<'a>(&'a toml_edit::Value);
impl Serialize for JsonValue<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[allow(clippy::redundant_pattern_matching)]
if let Some(v) = self.0.as_integer() {
v.serialize(serializer)
} else if let Some(v) = self.0.as_float() {
v.serialize(serializer)
} else if let Some(v) = self.0.as_bool() {
v.serialize(serializer)
} else if let Some(v) = self.0.as_str() {
v.serialize(serializer)
} else if let Some(_) = self.0.as_datetime() {
"UNIMPLEMENTED: DateTime".serialize(serializer) } else if let Some(arr) = self.0.as_array() {
let mut seq = serializer.serialize_seq(Some(arr.len()))?;
for e in arr.iter() {
seq.serialize_element(&JsonValue(e))?;
}
seq.end()
} else if let Some(t) = self.0.as_inline_table() {
let mut map = serializer.serialize_map(Some(t.len()))?;
for (k, v) in t.iter() {
map.serialize_entry(k, &JsonValue(v))?;
}
map.end()
} else {
panic!("unknown variant of toml_edit::Value");
}
}
}