use std::fmt;
use std::path::{Path, PathBuf};
use crate::{Value, value};
use anyhow::{Context, bail};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
Toml,
}
impl Format {
pub fn from_path(path: &Path) -> Option<Self> {
let ext = path.extension()?.to_str()?;
if ext.eq_ignore_ascii_case("json") {
Some(Self::Json)
} else if ext.eq_ignore_ascii_case("toml") {
Some(Self::Toml)
} else {
None
}
}
pub fn extension(self) -> &'static str {
match self {
Self::Json => "json",
Self::Toml => "toml",
}
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.extension())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceName {
File(PathBuf),
Stdin,
}
impl fmt::Display for SourceName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(p) => write!(f, "{}", p.display()),
Self::Stdin => f.write_str("<stdin>"),
}
}
}
pub fn parse(format: Format, text: &str, source: &SourceName) -> anyhow::Result<Value> {
let value = match format {
Format::Json => {
let native: serde_json::Value =
serde_json::from_str(text).with_context(|| format!("{source}: invalid JSON"))?;
value::from_json(native)
}
Format::Toml => {
let native: toml::Value =
toml::from_str(text).with_context(|| format!("{source}: invalid TOML"))?;
value::from_toml(native)
}
};
if !matches!(value, Value::Object(_)) {
bail!(
"{source}: expected an object at the top level, found {}",
value.kind()
);
}
Ok(value)
}
pub fn emit(
value: Value,
format: Format,
pretty: bool,
null_as: Option<&str>,
) -> anyhow::Result<String> {
let text = match format {
Format::Json => {
let native = value::to_json(value)?;
if pretty {
serde_json::to_string_pretty(&native)?
} else {
serde_json::to_string(&native)?
}
}
Format::Toml => {
let mut value = value;
if let Some(placeholder) = null_as {
value::replace_nulls(&mut value, placeholder);
}
let native = value::to_toml(value)?;
if pretty {
toml::to_string_pretty(&native)?
} else {
toml::to_string(&native)?
}
}
};
Ok(ensure_trailing_newline(text))
}
fn ensure_trailing_newline(mut s: String) -> String {
if !s.ends_with('\n') {
s.push('\n');
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extension_inference() {
assert_eq!(Format::from_path(Path::new("a.json")), Some(Format::Json));
assert_eq!(Format::from_path(Path::new("a.TOML")), Some(Format::Toml));
assert_eq!(Format::from_path(Path::new("a.yaml")), None);
assert_eq!(Format::from_path(Path::new("a")), None);
}
#[test]
fn top_level_must_be_an_object() {
let err = parse(Format::Json, "[1,2]", &SourceName::Stdin).unwrap_err();
assert!(err.to_string().contains("found array"), "{err}");
}
}