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