1use crate::error::{Error, Result};
4use crate::field_type::BpsvFieldType;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct BpsvField {
11 pub name: String,
13 pub field_type: BpsvFieldType,
15 pub index: usize,
17}
18
19impl BpsvField {
20 pub fn new(name: String, field_type: BpsvFieldType, index: usize) -> Self {
22 Self {
23 name,
24 field_type,
25 index,
26 }
27 }
28
29 pub fn validate_value(&self, value: &str) -> Result<String> {
31 self.field_type.validate_value(value).map_err(|mut err| {
32 if let Error::InvalidValue { field, .. } = &mut err {
33 *field = self.name.clone();
34 }
35 err
36 })
37 }
38}
39
40#[derive(Debug, Clone, PartialEq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct BpsvSchema {
44 fields: Vec<BpsvField>,
46 field_map: HashMap<String, usize>,
48}
49
50impl BpsvSchema {
51 pub fn new() -> Self {
53 Self {
54 fields: Vec::new(),
55 field_map: HashMap::new(),
56 }
57 }
58
59 pub fn parse_header(header_line: &str) -> Result<Self> {
76 let mut schema = Self::new();
77
78 for field_spec in header_line.split('|') {
79 let parts: Vec<&str> = field_spec.split('!').collect();
80 if parts.len() != 2 {
81 return Err(Error::InvalidHeader {
82 reason: format!("Invalid field specification: {field_spec}"),
83 });
84 }
85
86 let field_name = parts[0].to_string();
87 let type_spec = parts[1];
88
89 if schema.field_map.contains_key(&field_name) {
91 return Err(Error::DuplicateField { field: field_name });
92 }
93
94 let field_type = BpsvFieldType::parse(type_spec)?;
95 schema.add_field(field_name, field_type)?;
96 }
97
98 if schema.fields.is_empty() {
99 return Err(Error::InvalidHeader {
100 reason: "No fields found in header".to_string(),
101 });
102 }
103
104 Ok(schema)
105 }
106
107 pub fn add_field(&mut self, name: String, field_type: BpsvFieldType) -> Result<()> {
109 if self.field_map.contains_key(&name) {
110 return Err(Error::DuplicateField { field: name });
111 }
112
113 let index = self.fields.len();
114 let field = BpsvField::new(name.clone(), field_type, index);
115
116 self.fields.push(field);
117 self.field_map.insert(name, index);
118
119 Ok(())
120 }
121
122 pub fn field_count(&self) -> usize {
124 self.fields.len()
125 }
126
127 pub fn has_field(&self, name: &str) -> bool {
129 self.field_map.contains_key(name)
130 }
131
132 pub fn get_field(&self, name: &str) -> Option<&BpsvField> {
134 self.field_map.get(name).map(|&index| &self.fields[index])
135 }
136
137 pub fn get_field_by_index(&self, index: usize) -> Option<&BpsvField> {
139 self.fields.get(index)
140 }
141
142 pub fn fields(&self) -> &[BpsvField] {
144 &self.fields
145 }
146
147 pub fn field_names(&self) -> Vec<&str> {
149 self.fields.iter().map(|f| f.name.as_str()).collect()
150 }
151
152 pub fn validate_row(&self, values: &[String]) -> Result<Vec<String>> {
154 if values.len() != self.fields.len() {
155 return Err(Error::SchemaMismatch {
156 expected: self.fields.len(),
157 actual: values.len(),
158 });
159 }
160
161 let mut validated = Vec::new();
162 for (field, value) in self.fields.iter().zip(values.iter()) {
163 let normalized = field.validate_value(value)?;
164 validated.push(normalized);
165 }
166
167 Ok(validated)
168 }
169
170 pub fn validate_row_refs<'a>(&self, values: &[&'a str]) -> Result<Vec<&'a str>> {
172 if values.len() != self.fields.len() {
173 return Err(Error::SchemaMismatch {
174 expected: self.fields.len(),
175 actual: values.len(),
176 });
177 }
178
179 for (field, value) in self.fields.iter().zip(values.iter()) {
182 field.field_type.validate_value(value).map_err(|mut err| {
184 if let Error::InvalidValue {
185 field: field_name, ..
186 } = &mut err
187 {
188 *field_name = field.name.clone();
189 }
190 err
191 })?;
192 }
193
194 Ok(values.to_vec())
195 }
196
197 pub fn to_header_line(&self) -> String {
199 self.fields
200 .iter()
201 .map(|field| format!("{}!{}", field.name, field.field_type))
202 .collect::<Vec<_>>()
203 .join("|")
204 }
205}
206
207impl Default for BpsvSchema {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::BpsvFieldType;
217
218 #[test]
219 fn test_parse_header() {
220 let header = "Region!STRING:0|BuildConfig!HEX:16|BuildId!DEC:4";
221 let schema = BpsvSchema::parse_header(header).unwrap();
222
223 assert_eq!(schema.field_count(), 3);
224 assert!(schema.has_field("Region"));
225 assert!(schema.has_field("BuildConfig"));
226 assert!(schema.has_field("BuildId"));
227
228 let region_field = schema.get_field("Region").unwrap();
229 assert_eq!(region_field.field_type, BpsvFieldType::String(0));
230 assert_eq!(region_field.index, 0);
231
232 let build_field = schema.get_field("BuildConfig").unwrap();
233 assert_eq!(build_field.field_type, BpsvFieldType::Hex(16));
234 assert_eq!(build_field.index, 1);
235 }
236
237 #[test]
238 fn test_parse_header_case_insensitive() {
239 let header = "Region!string:0|BuildConfig!hex:16|BuildId!dec:4";
240 let schema = BpsvSchema::parse_header(header).unwrap();
241
242 assert_eq!(schema.field_count(), 3);
243
244 let region_field = schema.get_field("Region").unwrap();
245 assert_eq!(region_field.field_type, BpsvFieldType::String(0));
246 }
247
248 #[test]
249 fn test_duplicate_field_error() {
250 let header = "Region!STRING:0|Region!HEX:16";
251 let result = BpsvSchema::parse_header(header);
252 assert!(matches!(result, Err(Error::DuplicateField { .. })));
253 }
254
255 #[test]
256 fn test_invalid_header_format() {
257 let header = "Region|BuildConfig!HEX:16"; let result = BpsvSchema::parse_header(header);
259 assert!(matches!(result, Err(Error::InvalidHeader { .. })));
260 }
261
262 #[test]
263 fn test_validate_row() {
264 let header = "Region!STRING:0|BuildConfig!HEX:16|BuildId!DEC:4";
265 let schema = BpsvSchema::parse_header(header).unwrap();
266
267 let valid_row = vec![
268 "us".to_string(),
269 "abcd1234abcd1234abcd1234abcd1234".to_string(),
270 "1234".to_string(),
271 ];
272 let result = schema.validate_row(&valid_row);
273 assert!(result.is_ok());
274
275 let invalid_row = vec![
276 "us".to_string(),
277 "invalid_hex".to_string(),
278 "1234".to_string(),
279 ];
280 let result = schema.validate_row(&invalid_row);
281 assert!(result.is_err());
282
283 let wrong_length = vec!["us".to_string()]; let result = schema.validate_row(&wrong_length);
285 assert!(matches!(result, Err(Error::SchemaMismatch { .. })));
286 }
287
288 #[test]
289 fn test_to_header_line() {
290 let mut schema = BpsvSchema::new();
291 schema
292 .add_field("Region".to_string(), BpsvFieldType::String(0))
293 .unwrap();
294 schema
295 .add_field("BuildConfig".to_string(), BpsvFieldType::Hex(16))
296 .unwrap();
297 schema
298 .add_field("BuildId".to_string(), BpsvFieldType::Decimal(4))
299 .unwrap();
300
301 let header_line = schema.to_header_line();
302 assert_eq!(
303 header_line,
304 "Region!STRING:0|BuildConfig!HEX:16|BuildId!DEC:4"
305 );
306 }
307
308 #[test]
309 fn test_field_access() {
310 let header = "Region!STRING:0|BuildConfig!HEX:16|BuildId!DEC:4";
311 let schema = BpsvSchema::parse_header(header).unwrap();
312
313 assert_eq!(
314 schema.field_names(),
315 vec!["Region", "BuildConfig", "BuildId"]
316 );
317
318 assert_eq!(schema.get_field_by_index(0).unwrap().name, "Region");
319 assert_eq!(schema.get_field_by_index(1).unwrap().name, "BuildConfig");
320 assert_eq!(schema.get_field_by_index(2).unwrap().name, "BuildId");
321 assert!(schema.get_field_by_index(3).is_none());
322 }
323}