altium_format/api/generic/
record.rs1use indexmap::IndexMap;
4
5use super::Value;
6use crate::types::ParameterCollection;
7
8#[derive(Debug, Clone)]
10struct FieldEntry {
11 original_key: String,
13 value: Value,
15 modified: bool,
17 original_value: Option<String>,
19}
20
21#[derive(Debug, Clone)]
26pub struct GenericRecord {
27 record_id: Option<i32>,
29 raw_data: String,
31 fields: IndexMap<String, FieldEntry>,
33}
34
35impl GenericRecord {
36 pub fn from_params(params: &ParameterCollection) -> Self {
38 let mut fields = IndexMap::new();
39 let mut record_id = None;
40
41 for (key, pv) in params.iter() {
42 let value = Value::from_param_value(&pv);
43
44 if key.eq_ignore_ascii_case("RECORD") {
46 record_id = value.as_int().map(|i| i as i32);
47 }
48
49 fields.insert(
50 key.to_uppercase(),
51 FieldEntry {
52 original_key: key.to_string(),
53 value,
54 modified: false,
55 original_value: Some(pv.as_str().to_string()),
56 },
57 );
58 }
59
60 GenericRecord {
61 record_id,
62 raw_data: params.to_string(),
63 fields,
64 }
65 }
66
67 pub fn new(record_id: i32) -> Self {
69 let mut record = GenericRecord {
70 record_id: Some(record_id),
71 raw_data: String::new(),
72 fields: IndexMap::new(),
73 };
74 record.set("RECORD", Value::Int(record_id as i64));
75 record
76 }
77
78 pub fn record_id(&self) -> Option<i32> {
82 self.record_id
83 }
84
85 pub fn type_name(&self) -> &'static str {
87 match self.record_id {
88 Some(1) => "Component",
89 Some(2) => "Pin",
90 Some(3) => "Symbol",
91 Some(4) => "Text",
92 Some(5) => "Bezier",
93 Some(6) => "Polyline",
94 Some(7) => "Polygon",
95 Some(8) => "Ellipse",
96 Some(9) => "Piechart",
97 Some(10) => "RectangleBorder",
98 Some(11) => "SymbolBorder",
99 Some(12) => "GraphicBody",
100 Some(13) => "Arc",
101 Some(14) => "Line",
102 Some(15) => "Rectangle",
103 Some(17) => "PowerPort",
104 Some(18) => "Port",
105 Some(25) => "NetLabel",
106 Some(26) => "Bus",
107 Some(27) => "Wire",
108 Some(28) => "Junction",
109 Some(29) => "Image",
110 Some(30) => "Sheet",
111 Some(31) => "SheetName",
112 Some(32) => "FileName",
113 Some(33) => "Designator",
114 Some(34) => "BusEntry",
115 Some(37) => "Template",
116 Some(39) => "Parameter",
117 Some(41) => "ParameterSet",
118 Some(44) => "OffSheet",
119 Some(45) => "Harness",
120 Some(46) => "HarnessEntry",
121 Some(47) => "HarnessType",
122 Some(48) => "Implementation",
123 Some(215) => "Note",
124 Some(226) => "CompileMessage",
125 Some(id) => {
126 if id > 0 { "Record" } else { "Unknown" }
128 }
129 None => "Unknown",
130 }
131 }
132
133 pub fn contains(&self, key: &str) -> bool {
137 self.fields.contains_key(&key.to_uppercase())
138 }
139
140 pub fn get(&self, key: &str) -> Option<&Value> {
142 self.fields.get(&key.to_uppercase()).map(|e| &e.value)
143 }
144
145 pub fn get_or<'a>(&'a self, key: &str, default: &'a Value) -> &'a Value {
147 self.get(key).unwrap_or(default)
148 }
149
150 pub fn get_bool(&self, key: &str) -> Option<bool> {
154 self.get(key).and_then(|v| v.as_bool())
155 }
156
157 pub fn get_int(&self, key: &str) -> Option<i64> {
159 self.get(key).and_then(|v| v.as_int())
160 }
161
162 pub fn get_float(&self, key: &str) -> Option<f64> {
164 self.get(key).and_then(|v| v.as_float())
165 }
166
167 pub fn get_str(&self, key: &str) -> Option<&str> {
169 self.get(key).and_then(|v| v.as_str())
170 }
171
172 pub fn get_coord(&self, key: &str) -> Option<crate::types::Coord> {
174 self.get(key).and_then(|v| v.as_coord())
175 }
176
177 pub fn get_color(&self, key: &str) -> Option<crate::types::Color> {
179 self.get(key).and_then(|v| v.as_color())
180 }
181
182 pub fn get_layer(&self, key: &str) -> Option<crate::types::Layer> {
184 self.get(key).and_then(|v| v.as_layer())
185 }
186
187 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
191 self.fields.iter().map(|(k, e)| (k.as_str(), &e.value))
192 }
193
194 pub fn keys(&self) -> impl Iterator<Item = &str> {
196 self.fields.keys().map(|k| k.as_str())
197 }
198
199 pub fn values(&self) -> impl Iterator<Item = &Value> {
201 self.fields.values().map(|e| &e.value)
202 }
203
204 pub fn len(&self) -> usize {
206 self.fields.len()
207 }
208
209 pub fn is_empty(&self) -> bool {
211 self.fields.is_empty()
212 }
213
214 pub fn set(&mut self, key: &str, value: impl Into<Value>) {
218 let upper_key = key.to_uppercase();
219 let value = value.into();
220
221 if upper_key == "RECORD" {
223 self.record_id = value.as_int().map(|i| i as i32);
224 }
225
226 if let Some(entry) = self.fields.get_mut(&upper_key) {
227 entry.value = value;
228 entry.modified = true;
229 } else {
230 self.fields.insert(
231 upper_key,
232 FieldEntry {
233 original_key: key.to_string(),
234 value,
235 modified: true,
236 original_value: None,
237 },
238 );
239 }
240 }
241
242 pub fn remove(&mut self, key: &str) -> Option<Value> {
244 self.fields
245 .swap_remove(&key.to_uppercase())
246 .map(|e| e.value)
247 }
248
249 pub fn is_modified(&self) -> bool {
251 self.fields.values().any(|e| e.modified)
252 }
253
254 pub fn modified_fields(&self) -> Vec<&str> {
256 self.fields
257 .iter()
258 .filter(|(_, e)| e.modified)
259 .map(|(k, _)| k.as_str())
260 .collect()
261 }
262
263 pub fn reset(&mut self) {
265 for entry in self.fields.values_mut() {
266 if entry.modified {
267 if let Some(ref original) = entry.original_value {
268 entry.value = Value::String(original.clone());
270 entry.modified = false;
271 }
272 }
273 }
274 }
275
276 pub fn to_params(&self) -> ParameterCollection {
280 let mut params = ParameterCollection::new();
281
282 for (_, entry) in &self.fields {
283 let value_str = match &entry.value {
284 Value::Null => continue,
285 Value::Bool(b) => if *b { "T" } else { "F" }.to_string(),
286 Value::Int(i) => i.to_string(),
287 Value::Float(f) => format!("{}", f),
288 Value::String(s) => s.clone(),
289 Value::Coord(c) => format!("{}mil", c.to_mils()),
290 Value::Color(c) => c.to_win32().to_string(),
291 Value::Layer(l) => l.0.to_string(),
292 Value::List(l) => l
293 .iter()
294 .map(|v| v.to_string())
295 .collect::<Vec<_>>()
296 .join(","),
297 Value::Binary(_) => continue, };
299
300 params.add(&entry.original_key, &value_str);
301 }
302
303 params
304 }
305
306 pub fn to_params_string(&self) -> String {
308 self.to_params().to_string()
309 }
310
311 pub fn raw_data(&self) -> &str {
313 &self.raw_data
314 }
315
316 pub fn owner_index(&self) -> i32 {
320 self.get_int("OWNERINDEX").unwrap_or(-1) as i32
321 }
322
323 pub fn set_owner_index(&mut self, index: i32) {
325 self.set("OWNERINDEX", Value::Int(index as i64));
326 }
327}
328
329impl Default for GenericRecord {
330 fn default() -> Self {
331 GenericRecord {
332 record_id: None,
333 raw_data: String::new(),
334 fields: IndexMap::new(),
335 }
336 }
337}
338
339impl std::ops::Index<&str> for GenericRecord {
340 type Output = Value;
341
342 fn index(&self, key: &str) -> &Value {
343 static NULL: Value = Value::Null;
344 self.get(key).unwrap_or(&NULL)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_from_params() {
354 let params = ParameterCollection::from_string("|RECORD=1|NAME=Test|VALUE=42|");
355 let record = GenericRecord::from_params(¶ms);
356
357 assert_eq!(record.record_id(), Some(1));
358 assert_eq!(record.get_str("NAME"), Some("Test"));
359 assert_eq!(record.get_int("VALUE"), Some(42));
360 }
361
362 #[test]
363 fn test_modification_tracking() {
364 let params = ParameterCollection::from_string("|RECORD=1|NAME=Test|");
365 let mut record = GenericRecord::from_params(¶ms);
366
367 assert!(!record.is_modified());
368
369 record.set("NAME", "Modified");
370 assert!(record.is_modified());
371 assert_eq!(record.modified_fields(), vec!["NAME"]);
372 }
373
374 #[test]
375 fn test_order_preservation() {
376 let params = ParameterCollection::from_string("|A=1|B=2|C=3|");
377 let record = GenericRecord::from_params(¶ms);
378
379 let keys: Vec<_> = record.keys().collect();
380 assert_eq!(keys, vec!["A", "B", "C"]);
381 }
382
383 #[test]
384 fn test_index_operator() {
385 let params = ParameterCollection::from_string("|NAME=Test|");
386 let record = GenericRecord::from_params(¶ms);
387
388 assert_eq!(record["NAME"].as_str(), Some("Test"));
389 assert!(record["MISSING"].is_null());
390 }
391}