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)]
55pub enum SourceName {
56 File(PathBuf),
57 Stdin,
58 Set(String),
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 Self::Set(expr) => write!(f, "--set {expr}"),
67 }
68 }
69}
70
71pub fn parse(format: Format, text: &str, source: &SourceName) -> anyhow::Result<Value> {
77 let value = match format {
78 Format::Json => {
79 let native: serde_json::Value =
80 serde_json::from_str(text).with_context(|| format!("{source}: invalid JSON"))?;
81 value::from_json(native)
82 }
83 Format::Toml => {
84 let native: toml::Value =
85 toml::from_str(text).with_context(|| format!("{source}: invalid TOML"))?;
86 value::from_toml(native)
87 }
88 };
89 if !matches!(value, Value::Object(_)) {
90 bail!(
91 "{source}: expected an object at the top level, found {}",
92 value.kind()
93 );
94 }
95 Ok(value)
96}
97
98pub fn emit(
105 value: Value,
106 format: Format,
107 pretty: bool,
108 sources: &[(SourceName, Value)],
109) -> anyhow::Result<String> {
110 let text = match format {
111 Format::Json => {
112 let native = value::to_json(value);
113 if pretty {
114 serde_json::to_string_pretty(&native)?
115 } else {
116 serde_json::to_string(&native)?
117 }
118 }
119 Format::Toml => {
120 let native = value::to_toml(value).map_err(|e| e.with_origins(sources))?;
121 if pretty {
122 toml::to_string_pretty(&native)?
123 } else {
124 toml::to_string(&native)?
125 }
126 }
127 };
128 Ok(ensure_trailing_newline(text))
129}
130
131fn ensure_trailing_newline(mut s: String) -> String {
132 if !s.ends_with('\n') {
133 s.push('\n');
134 }
135 s
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn extension_inference() {
144 assert_eq!(Format::from_path(Path::new("a.json")), Some(Format::Json));
145 assert_eq!(Format::from_path(Path::new("a.TOML")), Some(Format::Toml));
146 assert_eq!(Format::from_path(Path::new("a.yaml")), None);
147 assert_eq!(Format::from_path(Path::new("a")), None);
148 }
149
150 #[test]
151 fn top_level_must_be_an_object() {
152 let err = parse(Format::Json, "[1,2]", &SourceName::Stdin).unwrap_err();
153 assert!(err.to_string().contains("found array"), "{err}");
154 }
155}