Skip to main content

altium_format/api/generic/
record.rs

1//! GenericRecord for parameter-format records.
2
3use indexmap::IndexMap;
4
5use super::Value;
6use crate::types::ParameterCollection;
7
8/// Entry for a field in a GenericRecord.
9#[derive(Debug, Clone)]
10struct FieldEntry {
11    /// Original key as it appeared in the source
12    original_key: String,
13    /// Current value
14    value: Value,
15    /// Whether this field was modified
16    modified: bool,
17    /// Original raw value (for non-destructive editing)
18    original_value: Option<String>,
19}
20
21/// A record with dynamic field access for parameter format.
22///
23/// Preserves original field order and tracks modifications for
24/// non-destructive round-trip editing.
25#[derive(Debug, Clone)]
26pub struct GenericRecord {
27    /// RECORD parameter value (schematic record ID)
28    record_id: Option<i32>,
29    /// Original raw parameter string
30    raw_data: String,
31    /// Fields in original order
32    fields: IndexMap<String, FieldEntry>,
33}
34
35impl GenericRecord {
36    /// Creates a GenericRecord from a ParameterCollection.
37    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            // Extract record ID
45            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    /// Creates an empty GenericRecord with a given record ID.
68    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    // --- Record Type ---
79
80    /// Returns the record ID (RECORD parameter).
81    pub fn record_id(&self) -> Option<i32> {
82        self.record_id
83    }
84
85    /// Returns a human-readable type name based on record ID.
86    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                // Can't return dynamic string, return generic name
127                if id > 0 { "Record" } else { "Unknown" }
128            }
129            None => "Unknown",
130        }
131    }
132
133    // --- Field Access ---
134
135    /// Returns true if the field exists.
136    pub fn contains(&self, key: &str) -> bool {
137        self.fields.contains_key(&key.to_uppercase())
138    }
139
140    /// Gets a field value.
141    pub fn get(&self, key: &str) -> Option<&Value> {
142        self.fields.get(&key.to_uppercase()).map(|e| &e.value)
143    }
144
145    /// Gets a field value with a default.
146    pub fn get_or<'a>(&'a self, key: &str, default: &'a Value) -> &'a Value {
147        self.get(key).unwrap_or(default)
148    }
149
150    // --- Typed getters ---
151
152    /// Gets a field as a boolean.
153    pub fn get_bool(&self, key: &str) -> Option<bool> {
154        self.get(key).and_then(|v| v.as_bool())
155    }
156
157    /// Gets a field as an integer.
158    pub fn get_int(&self, key: &str) -> Option<i64> {
159        self.get(key).and_then(|v| v.as_int())
160    }
161
162    /// Gets a field as a float.
163    pub fn get_float(&self, key: &str) -> Option<f64> {
164        self.get(key).and_then(|v| v.as_float())
165    }
166
167    /// Gets a field as a string.
168    pub fn get_str(&self, key: &str) -> Option<&str> {
169        self.get(key).and_then(|v| v.as_str())
170    }
171
172    /// Gets a field as a coordinate.
173    pub fn get_coord(&self, key: &str) -> Option<crate::types::Coord> {
174        self.get(key).and_then(|v| v.as_coord())
175    }
176
177    /// Gets a field as a color.
178    pub fn get_color(&self, key: &str) -> Option<crate::types::Color> {
179        self.get(key).and_then(|v| v.as_color())
180    }
181
182    /// Gets a field as a layer.
183    pub fn get_layer(&self, key: &str) -> Option<crate::types::Layer> {
184        self.get(key).and_then(|v| v.as_layer())
185    }
186
187    // --- Iteration ---
188
189    /// Iterates over all fields in original order.
190    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
191        self.fields.iter().map(|(k, e)| (k.as_str(), &e.value))
192    }
193
194    /// Returns field keys in original order.
195    pub fn keys(&self) -> impl Iterator<Item = &str> {
196        self.fields.keys().map(|k| k.as_str())
197    }
198
199    /// Returns field values in original order.
200    pub fn values(&self) -> impl Iterator<Item = &Value> {
201        self.fields.values().map(|e| &e.value)
202    }
203
204    /// Returns the number of fields.
205    pub fn len(&self) -> usize {
206        self.fields.len()
207    }
208
209    /// Returns true if there are no fields.
210    pub fn is_empty(&self) -> bool {
211        self.fields.is_empty()
212    }
213
214    // --- Modification ---
215
216    /// Sets a field value, preserving position if it exists.
217    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        // Update record_id if setting RECORD
222        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    /// Removes a field and returns its value.
243    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    /// Returns true if any fields were modified.
250    pub fn is_modified(&self) -> bool {
251        self.fields.values().any(|e| e.modified)
252    }
253
254    /// Returns the names of modified fields.
255    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    /// Resets all modifications, restoring original values.
264    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                    // Re-parse original value
269                    entry.value = Value::String(original.clone());
270                    entry.modified = false;
271                }
272            }
273        }
274    }
275
276    // --- Serialization ---
277
278    /// Converts back to a ParameterCollection.
279    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, // Skip binary in params
298            };
299
300            params.add(&entry.original_key, &value_str);
301        }
302
303        params
304    }
305
306    /// Converts to a parameter string.
307    pub fn to_params_string(&self) -> String {
308        self.to_params().to_string()
309    }
310
311    /// Returns the raw original data.
312    pub fn raw_data(&self) -> &str {
313        &self.raw_data
314    }
315
316    // --- Hierarchy ---
317
318    /// Gets the owner index (OWNERINDEX parameter).
319    pub fn owner_index(&self) -> i32 {
320        self.get_int("OWNERINDEX").unwrap_or(-1) as i32
321    }
322
323    /// Sets the owner index.
324    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(&params);
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(&params);
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(&params);
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(&params);
387
388        assert_eq!(record["NAME"].as_str(), Some("Test"));
389        assert!(record["MISSING"].is_null());
390    }
391}