Skip to main content

fmtcnv/
lib.rs

1mod csv_value;
2mod hocon_value;
3mod jsonl_value;
4mod xml_value;
5
6use anyhow::Result;
7use serde::Serialize;
8
9use crate::{
10	csv_value::{CsvWrapper, json_to_csv, load_csv},
11	hocon_value::{HoconWrapper, load_hocon},
12	jsonl_value::{JsonlWrapper, json_to_jsonl, load_jsonl},
13	xml_value::{XmlWrapper, json_to_xml, load_xml},
14};
15
16#[derive(Debug, Copy, Clone, PartialEq, clap::ValueEnum)]
17pub enum Format {
18	Bson,
19	Csv,
20	Hjson,
21	Hocon,
22	Json,
23	Json5,
24	Jsonl,
25	Plist,
26	Ron,
27	Toml,
28	Toon,
29	Xml,
30	Yaml,
31}
32
33#[derive(Debug, Serialize)]
34#[serde(untagged)]
35pub enum Value {
36	Bson(bson::Bson),
37	Csv(CsvWrapper),
38	Hjson(serde_hjson::Value),
39	Hocon(HoconWrapper),
40	Json(serde_json::Value),
41	Json5(serde_json::Value),
42	Jsonl(JsonlWrapper),
43	Plist(plist::Value),
44	Ron(ron::Value),
45	Toml(toml::Value),
46	Toon(serde_json::Value),
47	Xml(XmlWrapper),
48	Yaml(serde_yaml::Value),
49}
50
51pub fn load_input(input: &[u8], format: Format) -> Result<Value> {
52	let value = match format {
53		Format::Bson => Value::Bson(bson::deserialize_from_slice(input)?),
54		Format::Csv => Value::Csv(load_csv(input)?),
55		Format::Hjson => Value::Hjson(serde_hjson::from_slice(input)?),
56		Format::Hocon => Value::Hocon(load_hocon(input)?),
57		Format::Json => Value::Json(serde_json::from_slice(input)?),
58		Format::Json5 => Value::Json5(json5::from_str(str::from_utf8(input)?)?),
59		Format::Jsonl => Value::Jsonl(load_jsonl(input)?),
60		Format::Plist => Value::Plist(plist::from_bytes(input)?),
61		Format::Ron => Value::Ron(ron::de::from_bytes(input)?),
62		Format::Toml => {
63			let s = std::str::from_utf8(input)?;
64			Value::Toml(toml::from_str(s)?)
65		}
66		Format::Toon => {
67			let s = std::str::from_utf8(input)?;
68			Value::Toon(toon_format::decode_default(s)?)
69		}
70		Format::Xml => Value::Xml(load_xml(input)?),
71		Format::Yaml => Value::Yaml(serde_yaml::from_slice(input)?),
72	};
73	Ok(value)
74}
75
76pub fn dump_value(value: &Value, format: Format, is_compact: bool) -> Result<Vec<u8>> {
77	let dumped: Vec<u8> = match (format, is_compact) {
78		(Format::Bson, _) => bson::serialize_to_vec(value)?,
79		(Format::Csv, _) => {
80			let json_dumped = serde_json::to_vec(value)?;
81			json_to_csv(&json_dumped)?
82		}
83		(Format::Hjson, _) => serde_hjson::to_vec(value)?,
84		(Format::Hocon, true) => serde_json::to_vec(value)?,
85		(Format::Hocon, false) => serde_json::to_vec_pretty(value)?,
86		(Format::Json, true) => serde_json::to_vec(value)?,
87		(Format::Json, false) => serde_json::to_vec_pretty(value)?,
88		(Format::Json5, _) => json5::to_string(value).map(|e| e.into_bytes())?,
89		(Format::Jsonl, _) => {
90			let json_dumped = serde_json::to_vec(value)?;
91			json_to_jsonl(&json_dumped)?
92		}
93		(Format::Plist, _) => {
94			let mut buffer = Vec::new();
95			plist::to_writer_xml(&mut buffer, value)?;
96			buffer
97		}
98		(Format::Ron, true) => ron::ser::to_string(value).map(|e| e.into_bytes())?,
99		(Format::Ron, false) => ron::ser::to_string_pretty(value, ron::ser::PrettyConfig::default().new_line("\n".to_owned())).map(|e| e.into_bytes())?,
100		(Format::Toml, true) => toml::to_string(value).map(|e| e.into_bytes())?,
101		(Format::Toml, false) => toml::to_string_pretty(value).map(|e| e.into_bytes())?,
102		(Format::Toon, _) => toon_format::encode_default(value)?.as_bytes().to_vec(),
103		(Format::Xml, _) => {
104			let json_dumped = serde_json::to_vec(value)?;
105			json_to_xml(&json_dumped)?
106		}
107		(Format::Yaml, _) => serde_yaml::to_string(value).map(|e| e.into_bytes())?,
108	};
109	Ok(dumped)
110}
111
112#[cfg(test)]
113mod tests {
114	use rstest::rstest;
115
116	use super::*;
117
118	fn get_test_value(format: Format, is_compact: bool) -> String {
119		let value = match (format, is_compact) {
120			(Format::Bson, _) => {
121				"A\0\0\0\u{4}array\0\u{17}\0\0\0\u{2}0\0\u{2}\0\0\0a\0\u{2}1\0\u{2}\0\0\0b\0\0\u{8}boolean\0\0\u{12}the_answer\0*\0\0\0\0\0\0\0\0"
122			}
123			(Format::Csv, _) => unimplemented!("use raw data for tests"),
124			(Format::Hjson, _) => {
125				r#"{
126  array:
127  [
128    a
129    b
130  ]
131  boolean: false
132  the_answer: 42
133}"#
134			}
135			(Format::Hocon, _) => {
136				r#"
137array: [a,b]
138boolean: false
139the_answer: 42
140"#
141			}
142			(Format::Json, true) => r#"{"array":["a","b"],"boolean":false,"the_answer":42}"#,
143			(Format::Json, false) => {
144				r#"{
145  "array": [
146    "a",
147    "b"
148  ],
149  "boolean": false,
150  "the_answer": 42
151}"#
152			}
153			(Format::Json5, _) => {
154				r#"{
155  array: [
156    "a",
157    "b",
158  ],
159  boolean: false,
160  the_answer: 42,
161}"#
162			}
163			(Format::Jsonl, _) => unimplemented!("use raw data for tests"),
164			(Format::Plist, _) => {
165				r#"<?xml version="1.0" encoding="UTF-8"?>
166<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
167<plist version="1.0">
168<dict>
169	<key>array</key>
170	<array>
171		<string>a</string>
172		<string>b</string>
173	</array>
174	<key>boolean</key>
175	<false/>
176	<key>the_answer</key>
177	<integer>42</integer>
178</dict>
179</plist>"#
180			}
181			(Format::Ron, true) => r#"{"array":["a","b"],"boolean":false,"the_answer":42}"#,
182			(Format::Ron, false) => {
183				r#"{
184    "array": [
185        "a",
186        "b",
187    ],
188    "boolean": false,
189    "the_answer": 42,
190}"#
191			}
192			(Format::Toml, true) => {
193				r#"array = ["a", "b"]
194boolean = false
195the_answer = 42
196"#
197			}
198			(Format::Toml, false) => {
199				r#"array = [
200    "a",
201    "b",
202]
203boolean = false
204the_answer = 42
205"#
206			}
207			(Format::Toon, _) => {
208				r#"array[2]: a,b
209boolean: false
210the_answer: 42"#
211			}
212			(Format::Xml, _) => r#"<root><array>a</array><array>b</array><boolean>false</boolean><the_answer>42</the_answer></root>"#,
213			(Format::Yaml, _) => {
214				r#"array:
215- a
216- b
217boolean: false
218the_answer: 42
219"#
220			}
221		};
222		value.to_string()
223	}
224
225	#[rstest]
226	#[case(Format::Json, Format::Yaml, false)]
227	#[case(Format::Json, Format::Toml, true)]
228	#[case(Format::Json, Format::Toml, false)]
229	#[case(Format::Yaml, Format::Json, false)]
230	#[case(Format::Yaml, Format::Json, true)]
231	#[case(Format::Yaml, Format::Toml, true)]
232	#[case(Format::Toml, Format::Yaml, false)]
233	#[case(Format::Toml, Format::Json, true)]
234	#[case(Format::Json, Format::Ron, true)]
235	#[case(Format::Json, Format::Ron, false)]
236	#[case(Format::Ron, Format::Json, true)]
237	#[case(Format::Json5, Format::Json, true)]
238	#[case(Format::Json, Format::Json5, true)]
239	#[case(Format::Json, Format::Json5, false)]
240	#[case(Format::Json5, Format::Json, false)]
241	#[case(Format::Json, Format::Bson, false)]
242	#[case(Format::Bson, Format::Json5, true)]
243	#[case(Format::Hocon, Format::Json, false)]
244	#[case(Format::Xml, Format::Yaml, false)]
245	#[case(Format::Toml, Format::Xml, true)]
246	#[case(Format::Toml, Format::Hjson, true)]
247	#[case(Format::Hjson, Format::Json, false)]
248	#[case(Format::Toon, Format::Yaml, false)]
249	#[case(Format::Toon, Format::Json, true)]
250	#[case(Format::Yaml, Format::Toon, false)]
251	#[case(Format::Yaml, Format::Toon, true)]
252	#[case(Format::Json, Format::Plist, true)]
253	#[case(Format::Plist, Format::Yaml, true)]
254	fn test_convert_formats(#[case] from_format: Format, #[case] to_format: Format, #[case] is_compact: bool) {
255		println!("{from_format:?} -> {to_format:?}. is_compact: {is_compact}");
256
257		let input = get_test_value(from_format, is_compact);
258		let expected_output = get_test_value(to_format, is_compact);
259
260		let value = load_input(input.as_bytes(), from_format).unwrap();
261		let output = String::from_utf8(dump_value(&value, to_format, is_compact).unwrap()).unwrap();
262
263		assert_eq!(output, expected_output);
264	}
265
266	#[rstest]
267	#[case(
268		Format::Csv,
269		Format::Json,
270		r#"age,immortal,name,power
27155000,true,Gendalf,50.0
27250,false,Frodo,5.0
273"#,
274		r#"[
275  {
276    "age": 55000,
277    "immortal": true,
278    "name": "Gendalf",
279    "power": 50.0
280  },
281  {
282    "age": 50,
283    "immortal": false,
284    "name": "Frodo",
285    "power": 5.0
286  }
287]"#,
288		false
289	)]
290	#[case(
291        Format::Csv,
292        Format::Json,
293        r#"age,immortal,name,power,test_empty
29455000, true, Gendalf, 50.0,
29550, false, Frodo, 5.0,
296"#,
297        r#"[{"age":55000,"immortal":true,"name":"Gendalf","power":50.0,"test_empty":null},{"age":50,"immortal":false,"name":"Frodo","power":5.0,"test_empty":null}]"#,
298        true
299    )]
300	#[case(
301		Format::Json,
302		Format::Csv,
303		r#"[{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0},{"age":50,"immortal":false,"name":"Frodo","power":5.0}]"#,
304		r#"age,immortal,name,power
30555000,true,"Gendalf the \"White\"",50.0
30650,false,"Frodo",5.0
307"#,
308		true
309	)]
310	#[case(
311		Format::Hocon,
312		Format::Json,
313		r#"{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0}"#,
314		r#"{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0}"#,
315		true
316	)]
317	#[case(
318		Format::Json,
319		Format::Json,
320		r#"[{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0},{"age":50,"immortal":false,"name":"Frodo","power":5.0}]"#,
321		r#"[{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0},{"age":50,"immortal":false,"name":"Frodo","power":5.0}]"#,
322		true
323	)]
324	#[case(
325		Format::Jsonl,
326		Format::Xml,
327		r#"{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0}
328{"age":50,"immortal":false,"name":"Frodo","power":5.0}
329"#,
330		r#"<root><age>55000</age><immortal>true</immortal><name>Gendalf the &quot;White&quot;</name><power>50.0</power></root>
331<root><age>50</age><immortal>false</immortal><name>Frodo</name><power>5.0</power></root>
332"#,
333		false
334	)]
335	#[case(
336		Format::Json,
337		Format::Jsonl,
338		r#"[{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0},{"age":50,"immortal":false,"name":"Frodo","power":5.0}]"#,
339		r#"{"age":55000,"immortal":true,"name":"Gendalf the \"White\"","power":50.0}
340{"age":50,"immortal":false,"name":"Frodo","power":5.0}
341"#,
342		false
343	)]
344	fn test_raw_convert(#[case] from_format: Format, #[case] to_format: Format, #[case] input: &str, #[case] expected_output: &str, #[case] is_compact: bool) {
345		let value = load_input(input.as_bytes(), from_format).unwrap();
346		let output = String::from_utf8(dump_value(&value, to_format, is_compact).unwrap()).unwrap();
347
348		assert_eq!(output, expected_output);
349	}
350}