1use std::fs::File;
2use std::io::{BufReader, BufWriter};
3use std::path::Path;
4
5use crate::entry::Entry;
6use crate::error::{JMapError, Result};
7use crate::field::{Field, FieldType, FieldValue};
8use crate::hash::HashTable;
9use crate::jmap::JMapInfo;
10
11pub fn from_csv<H: HashTable, P: AsRef<Path>>(
27 hash_table: H,
28 path: P,
29 header_delimiter: Option<char>,
30
31) -> Result<JMapInfo<H>> {
32 let delimiter = header_delimiter.unwrap_or(':');
33 let file = File::open(path)?;
34 let reader = BufReader::new(file);
35 let mut csv_reader = csv::ReaderBuilder::new()
36 .has_headers(false)
37 .from_reader(reader);
38
39 let mut jmap = JMapInfo::new(hash_table);
40 let mut records = csv_reader.records();
41
42 let header = records
44 .next()
45 .ok_or_else(|| JMapError::CsvError("CSV file is empty".to_string()))??;
46
47 let mut field_infos: Vec<(u32, FieldType)> = Vec::new();
48
49 for field_desc in header.iter() {
50 let parts: Vec<&str> = field_desc.split(delimiter).collect();
51
52 if parts.len() != 3 {
53 return Err(JMapError::InvalidCsvFieldDescriptor(format!(
54 "Expected 3 parts (name{}type{}default), got: {}",
55 delimiter,
56 delimiter,
57 field_desc
58 )));
59 }
60
61 let field_name = parts[0];
62 let type_name = parts[1];
63 let _default_str = parts[2];
64
65 if field_name.is_empty() {
66 return Err(JMapError::InvalidCsvFieldDescriptor(
67 "Field name cannot be empty".to_string(),
68 ));
69 }
70
71 let field_type = FieldType::from_csv_name(type_name).ok_or_else(|| {
72 JMapError::InvalidCsvFieldDescriptor(format!("Unknown field type: {}", type_name))
73 })?;
74
75 let hash = if field_name.starts_with('[') && field_name.ends_with(']') {
77 let hex_str = &field_name[1..field_name.len() - 1];
78 u32::from_str_radix(hex_str, 16).map_err(|_| {
79 JMapError::InvalidCsvFieldDescriptor(format!("Invalid hash: {}", field_name))
80 })?
81 } else {
82 jmap.hash_table_mut().add(field_name)
83 };
84
85 let default = FieldValue::default_for(field_type);
86 let field = Field::with_default(hash, field_type, default);
87 jmap.fields_map_mut().insert(hash, field);
88 field_infos.push((hash, field_type));
89 }
90
91 for result in records {
93 let record = result?;
94 let mut entry = Entry::with_capacity(field_infos.len());
95
96 for (i, (hash, field_type)) in field_infos.iter().enumerate() {
97 let value_str = record.get(i).unwrap_or("");
98
99 let value = if value_str.is_empty() {
100 FieldValue::default_for(*field_type)
101 } else {
102 parse_field_value(value_str, *field_type)?
103 };
104
105 entry.set_by_hash(*hash, value);
106 }
107
108 jmap.entries_vec_mut().push(entry);
109 }
110
111 Ok(jmap)
112}
113
114pub fn to_csv<H: HashTable, P: AsRef<Path>>(jmap: &JMapInfo<H>, path: P, header_delimiter: Option<char>) -> Result<()> {
127 let delimiter = header_delimiter.unwrap_or(':');
128 let file = File::create(path)?;
129 let writer = BufWriter::new(file);
130 let mut csv_writer = csv::Writer::from_writer(writer);
131
132 let headers: Vec<String> = jmap
134 .fields()
135 .map(|field| {
136 let name = jmap.field_name(field.hash);
137 let type_name = field.field_type.csv_name();
138 let default = default_csv_value(field.field_type);
139 format!("{}{}{}{}{}", name, delimiter, type_name, delimiter, default)
140 })
141 .collect();
142
143 csv_writer.write_record(&headers)?;
144
145 for entry in jmap.entries() {
147 let values: Vec<String> = jmap
148 .fields()
149 .map(|field| {
150 entry
151 .get_by_hash(field.hash)
152 .map(|v| v.to_string())
153 .unwrap_or_default()
154 })
155 .collect();
156
157 csv_writer.write_record(&values)?;
158 }
159
160 csv_writer.flush()?;
161 Ok(())
162}
163
164fn parse_field_value(s: &str, field_type: FieldType) -> Result<FieldValue> {
165 match field_type {
166 FieldType::Long | FieldType::UnsignedLong | FieldType::Short | FieldType::Char => {
167 let v: i32 = s.parse().map_err(|_| {
168 JMapError::CsvError(format!("Cannot parse '{}' as integer", s))
169 })?;
170 Ok(FieldValue::Int(v))
171 }
172 FieldType::Float => {
173 let v: f32 = s.parse().map_err(|_| {
174 JMapError::CsvError(format!("Cannot parse '{}' as float", s))
175 })?;
176 Ok(FieldValue::Float(v))
177 }
178 FieldType::String | FieldType::StringOffset => Ok(FieldValue::String(s.to_string())),
179 }
180}
181
182fn default_csv_value(field_type: FieldType) -> &'static str {
183 match field_type {
184 FieldType::Long | FieldType::UnsignedLong | FieldType::Short | FieldType::Char => "0",
185 FieldType::Float => "0.0",
186 FieldType::String | FieldType::StringOffset => "0",
187 }
188}