1use std::fmt;
8use std::path::{Path, PathBuf};
9
10use crate::{Value, value};
11use anyhow::{Context, bail};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Format {
20 Json,
21 Toml,
22}
23
24impl Format {
25 pub fn from_path(path: &Path) -> Option<Self> {
28 let ext = path.extension()?.to_str()?;
29 if ext.eq_ignore_ascii_case("json") {
30 Some(Self::Json)
31 } else if ext.eq_ignore_ascii_case("toml") {
32 Some(Self::Toml)
33 } else {
34 None
35 }
36 }
37
38 pub fn extension(self) -> &'static str {
40 match self {
41 Self::Json => "json",
42 Self::Toml => "toml",
43 }
44 }
45}
46
47impl fmt::Display for Format {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(self.extension())
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum SourceName {
59 File(PathBuf),
60 Stdin,
61}
62
63impl fmt::Display for SourceName {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::File(p) => write!(f, "{}", p.display()),
67 Self::Stdin => f.write_str("<stdin>"),
68 }
69 }
70}
71
72pub fn parse(format: Format, text: &str, source: &SourceName) -> anyhow::Result<Value> {
78 let value = match format {
79 Format::Json => {
80 let native: serde_json::Value =
81 serde_json::from_str(text).with_context(|| format!("{source}: invalid JSON"))?;
82 value::from_json(native)
83 }
84 Format::Toml => {
85 let native: toml::Value =
86 toml::from_str(text).with_context(|| format!("{source}: invalid TOML"))?;
87 value::from_toml(native)
88 }
89 };
90 if !matches!(value, Value::Object(_)) {
91 bail!(
92 "{source}: expected an object at the top level, found {}",
93 value.kind()
94 );
95 }
96 Ok(value)
97}
98
99pub fn emit(
113 value: Value,
114 format: Format,
115 pretty: bool,
116 null_as: Option<&str>,
117) -> anyhow::Result<String> {
118 let text = match format {
119 Format::Json => {
120 let native = value::to_json(value)?;
121 if pretty {
122 serde_json::to_string_pretty(&native)?
123 } else {
124 serde_json::to_string(&native)?
125 }
126 }
127 Format::Toml => {
128 let mut value = value;
129 if let Some(placeholder) = null_as {
130 value::replace_nulls(&mut value, placeholder);
131 }
132 let native = value::to_toml(value)?;
133 if pretty {
134 toml::to_string_pretty(&native)?
135 } else {
136 toml::to_string(&native)?
137 }
138 }
139 };
140 Ok(ensure_trailing_newline(text))
141}
142
143fn ensure_trailing_newline(mut s: String) -> String {
144 if !s.ends_with('\n') {
145 s.push('\n');
146 }
147 s
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn extension_inference() {
156 assert_eq!(Format::from_path(Path::new("a.json")), Some(Format::Json));
157 assert_eq!(Format::from_path(Path::new("a.TOML")), Some(Format::Toml));
158 assert_eq!(Format::from_path(Path::new("a.yaml")), None);
159 assert_eq!(Format::from_path(Path::new("a")), None);
160 }
161
162 #[test]
163 fn top_level_must_be_an_object() {
164 let err = parse(Format::Json, "[1,2]", &SourceName::Stdin).unwrap_err();
165 assert!(err.to_string().contains("found array"), "{err}");
166 }
167}