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 "White"</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}