config_disassembler/
format.rs1use std::fs;
7use std::path::Path;
8use std::str::FromStr;
9
10use serde_json::Value;
11
12use crate::error::{Error, Result};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Format {
17 Json,
18 Json5,
19 Yaml,
20}
21
22impl Format {
23 pub fn extension(self) -> &'static str {
25 match self {
26 Format::Json => "json",
27 Format::Json5 => "json5",
28 Format::Yaml => "yaml",
29 }
30 }
31
32 pub fn from_path(path: &Path) -> Result<Self> {
34 let ext = path
35 .extension()
36 .and_then(|e| e.to_str())
37 .map(|e| e.to_ascii_lowercase());
38 match ext.as_deref() {
39 Some("json") => Ok(Format::Json),
40 Some("json5") => Ok(Format::Json5),
41 Some("yaml" | "yml") => Ok(Format::Yaml),
42 _ => Err(Error::UnknownFormat(path.to_path_buf())),
43 }
44 }
45
46 pub fn parse(self, input: &str) -> Result<Value> {
48 match self {
49 Format::Json => Ok(serde_json::from_str(input)?),
50 Format::Json5 => Ok(json5::from_str(input)?),
51 Format::Yaml => Ok(serde_yaml::from_str(input)?),
52 }
53 }
54
55 pub fn serialize(self, value: &Value) -> Result<String> {
58 let mut out = match self {
59 Format::Json => serde_json::to_string_pretty(value)?,
60 Format::Json5 => json5::to_string(value)?,
61 Format::Yaml => serde_yaml::to_string(value)?,
62 };
63 if !out.ends_with('\n') {
64 out.push('\n');
65 }
66 Ok(out)
67 }
68
69 pub fn load(self, path: &Path) -> Result<Value> {
71 let text = fs::read_to_string(path)?;
72 self.parse(&text)
73 }
74}
75
76impl FromStr for Format {
77 type Err = Error;
78
79 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
80 match s.to_ascii_lowercase().as_str() {
81 "json" => Ok(Format::Json),
82 "json5" => Ok(Format::Json5),
83 "yaml" | "yml" => Ok(Format::Yaml),
84 other => Err(Error::Usage(format!(
85 "unknown format `{other}`; expected json, json5, or yaml"
86 ))),
87 }
88 }
89}
90
91impl std::fmt::Display for Format {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.write_str(self.extension())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn from_str_accepts_canonical_and_aliases() {
103 assert_eq!("json".parse::<Format>().unwrap(), Format::Json);
104 assert_eq!("JSON5".parse::<Format>().unwrap(), Format::Json5);
105 assert_eq!("yaml".parse::<Format>().unwrap(), Format::Yaml);
106 assert_eq!("yml".parse::<Format>().unwrap(), Format::Yaml);
107 }
108
109 #[test]
110 fn from_str_rejects_unknown() {
111 let err = "xml".parse::<Format>().unwrap_err();
112 assert!(err.to_string().contains("unknown format"));
113 }
114
115 #[test]
116 fn from_path_detects_supported_extensions() {
117 assert_eq!(
118 Format::from_path(Path::new("a.json")).unwrap(),
119 Format::Json
120 );
121 assert_eq!(
122 Format::from_path(Path::new("a.JSON5")).unwrap(),
123 Format::Json5
124 );
125 assert_eq!(Format::from_path(Path::new("a.yml")).unwrap(), Format::Yaml);
126 }
127
128 #[test]
129 fn from_path_rejects_missing_or_unknown_extension() {
130 assert!(Format::from_path(Path::new("a")).is_err());
131 assert!(Format::from_path(Path::new("a.toml")).is_err());
132 }
133
134 #[test]
135 fn display_matches_extension() {
136 assert_eq!(Format::Json.to_string(), "json");
137 assert_eq!(Format::Json5.to_string(), "json5");
138 assert_eq!(Format::Yaml.to_string(), "yaml");
139 }
140
141 #[test]
142 fn parse_and_serialize_round_trip_for_all_formats() {
143 for (fmt, text) in [
144 (Format::Json, r#"{"a":1}"#),
145 (Format::Json5, "{ a: 1 }"),
146 (Format::Yaml, "a: 1\n"),
147 ] {
148 let v = fmt.parse(text).unwrap();
149 let out = fmt.serialize(&v).unwrap();
150 assert!(out.ends_with('\n'));
151 assert_eq!(fmt.parse(&out).unwrap(), v);
152 }
153 }
154}