1use std::{collections::HashMap, io::BufRead};
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 error::Error,
12 traits::Parser,
13 types::{Entry, EntryStatus, Metadata, Resource, Translation},
14};
15
16#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
17pub struct CSVRecord {
18 pub key: String,
19 pub value: String,
20}
21
22impl Parser for Vec<CSVRecord> {
23 fn from_reader<R: BufRead>(reader: R) -> Result<Self, Error> {
25 let mut rdr = csv::ReaderBuilder::new()
26 .has_headers(false)
27 .from_reader(reader);
28 let mut records = Vec::new();
29 for result in rdr.deserialize() {
30 records.push(result?);
31 }
32 Ok(records)
33 }
34
35 fn to_writer<W: std::io::Write>(&self, writer: W) -> Result<(), Error> {
37 let mut wtr = csv::WriterBuilder::new().from_writer(writer);
38 for record in self {
39 wtr.serialize(record)?;
40 }
41 wtr.flush()?;
42 Ok(())
43 }
44}
45
46impl From<Vec<CSVRecord>> for Resource {
47 fn from(value: Vec<CSVRecord>) -> Self {
48 Resource {
49 metadata: Metadata {
50 language: String::from(""),
51 domain: String::from(""),
52 custom: HashMap::new(),
53 },
54 entries: value
55 .into_iter()
56 .map(|record| {
57 Entry {
58 id: record.key,
59 value: Translation::Singular(record.value),
60 comment: None,
61 status: EntryStatus::Translated, custom: HashMap::new(),
63 }
64 })
65 .collect(),
66 }
67 }
68}
69
70impl TryFrom<Resource> for Vec<CSVRecord> {
71 type Error = Error;
72
73 fn try_from(value: Resource) -> Result<Self, Self::Error> {
74 Ok(value
75 .entries
76 .into_iter()
77 .map(|entry| CSVRecord {
78 key: entry.id.clone(),
79 value: match entry.value {
80 Translation::Singular(v) => v,
81 Translation::Plural(_) => String::new(), },
83 })
84 .collect())
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::traits::Parser;
92 use crate::types::{Resource, Translation};
93 use std::io::Cursor;
94
95 #[test]
96 fn test_parse_simple_csv() {
97 let csv_content = "hello,Hello\nbye,Goodbye\n";
98 let records = Vec::<CSVRecord>::from_reader(Cursor::new(csv_content)).unwrap();
99 assert_eq!(records.len(), 2);
100 assert_eq!(records[0].key, "hello");
101 assert_eq!(records[0].value, "Hello");
102 assert_eq!(records[1].key, "bye");
103 assert_eq!(records[1].value, "Goodbye");
104 }
105
106 #[test]
107 fn test_round_trip_csv_resource_csv() {
108 let csv_content = "hello,Hello\nbye,Goodbye\n";
109 let records = Vec::<CSVRecord>::from_reader(Cursor::new(csv_content)).unwrap();
110 let resource = Resource::from(records.clone());
111 let serialized: Vec<CSVRecord> = TryFrom::try_from(resource).unwrap();
112 assert_eq!(records, serialized);
114 }
115
116 #[test]
117 fn test_csv_row_with_empty_value() {
118 let csv_content = "empty,\nhello,Hello\n";
119 let records = Vec::<CSVRecord>::from_reader(Cursor::new(csv_content)).unwrap();
120 assert_eq!(records.len(), 2);
121 assert_eq!(records[0].key, "empty");
122 assert_eq!(records[0].value, "");
123 let resource = Resource::from(records.clone());
124 assert_eq!(resource.entries.len(), 2);
125 let entry = &resource.entries[0];
127 assert_eq!(entry.id, "empty");
128 assert_eq!(
129 match &entry.value {
130 Translation::Singular(s) => s,
131 _ => panic!("Expected singular translation"),
132 },
133 ""
134 );
135 }
136}