Skip to content

Commit 4d2a022

Browse files
committed
Parse IPC BOM alternatives, path
1 parent 1a531ae commit 4d2a022

File tree

1 file changed

+49
-33
lines changed
  • crates/pcb-ipc2581-tools/src/commands

1 file changed

+49
-33
lines changed

crates/pcb-ipc2581-tools/src/commands/bom.rs

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ use crate::OutputFormat;
1616
pub struct CharacteristicsData {
1717
pub package: Option<String>,
1818
pub value: Option<String>,
19+
pub path: Option<String>,
20+
pub alternatives: Vec<pcb_sch::Alternative>,
1921
pub properties: std::collections::BTreeMap<String, String>,
2022
}
2123

2224
/// Extract characteristics from IPC-2581 Characteristics
23-
/// Returns package, value, and custom properties
25+
/// Returns package, value, alternatives, and custom properties
2426
/// Note: MPN and Manufacturer must come from AVL/Enterprise (canonical IPC-2581 way)
2527
pub fn extract_characteristics(
2628
ipc: &ipc2581::Ipc2581,
@@ -37,12 +39,18 @@ pub fn extract_characteristics(
3739
match name_lower.as_str() {
3840
"package" | "footprint" => data.package = Some(val_str),
3941
"value" => data.value = Some(val_str),
40-
// Exclude well-known fields and instance-specific metadata
42+
"path" => data.path = Some(val_str),
43+
"alternatives" => {
44+
if let Some(alternative) = parse_alternative_json(&val_str) {
45+
data.alternatives.push(alternative);
46+
}
47+
}
48+
// Exclude well-known fields (MPN/Manufacturer come from AVL)
49+
// and instance-specific metadata
4150
"mpn"
4251
| "manufacturerpartnumber"
4352
| "partnumber"
4453
| "manufacturer"
45-
| "path"
4654
| "prefix"
4755
| "symbol_name"
4856
| "symbol_path" => {}
@@ -56,6 +64,22 @@ pub fn extract_characteristics(
5664
data
5765
}
5866

67+
/// Parse alternative part data from JSON string
68+
/// Handles HTML-encoded JSON like: {&quot;mpn&quot;: &quot;...&quot;, &quot;manufacturer&quot;: &quot;...&quot;}
69+
fn parse_alternative_json(json_str: &str) -> Option<pcb_sch::Alternative> {
70+
// Decode HTML entities using quick-xml
71+
let decoded = quick_xml::escape::unescape(json_str).ok()?.to_string();
72+
73+
// Parse as JSON
74+
let parsed: serde_json::Value = serde_json::from_str(&decoded).ok()?;
75+
76+
// Extract mpn and manufacturer
77+
let mpn = parsed.get("mpn")?.as_str()?.to_string();
78+
let manufacturer = parsed.get("manufacturer")?.as_str()?.to_string();
79+
80+
Some(pcb_sch::Alternative { mpn, manufacturer })
81+
}
82+
5983
pub fn execute(file: &Path, format: OutputFormat) -> Result<()> {
6084
let content = file_utils::load_ipc_file(file)?;
6185
let ipc = ipc2581::Ipc2581::parse(&content)?;
@@ -93,6 +117,8 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
93117
let CharacteristicsData {
94118
package,
95119
value,
120+
path: component_path,
121+
alternatives: textual_alternatives,
96122
properties,
97123
} = item
98124
.characteristics
@@ -104,6 +130,10 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
104130
let (mpn, manufacturer, avl_alternatives) =
105131
lookup_from_avl(ipc, item.oem_design_number_ref);
106132

133+
// Merge alternatives: AVL takes precedence, then textual characteristics
134+
let mut alternatives = avl_alternatives;
135+
alternatives.extend(textual_alternatives);
136+
107137
// Use BomItem description attribute if present, otherwise fallback to value
108138
let description = item
109139
.description
@@ -113,35 +143,30 @@ fn extract_bom_from_ipc(ipc: &ipc2581::Ipc2581) -> Result<Bom> {
113143
// Build entry
114144
let entry = BomEntry {
115145
mpn,
116-
alternatives: avl_alternatives,
146+
alternatives,
117147
manufacturer,
118148
package,
119149
value,
120150
description,
121151
generic_data: None,
122152
offers: Vec::new(),
123-
dnp: false, // Check ref des for populate flag
153+
dnp: false, // Will be set per ref_des
124154
skip_bom: false,
125155
properties,
126156
};
127157

128158
// Process reference designators
129159
for ref_des in &item.ref_des_list {
130160
let designator = ipc.resolve(ref_des.name).to_string();
131-
132-
// Skip empty designators (invalid/placeholder entries)
133161
if designator.is_empty() {
134162
continue;
135163
}
136164

137-
// Use Path property if available, otherwise use ipc::designator format
138-
let path = entry
139-
.properties
140-
.get("Path")
141-
.cloned()
165+
// Use Path from characteristics, or fallback to ipc::designator format
166+
let path = component_path
167+
.clone()
142168
.unwrap_or_else(|| format!("ipc::{}", designator));
143169

144-
// Check DNP status
145170
let mut entry_with_dnp = entry.clone();
146171
entry_with_dnp.dnp = !ref_des.populate;
147172

@@ -198,23 +223,17 @@ pub fn lookup_from_avl(
198223
ipc: &ipc2581::Ipc2581,
199224
oem_design_number_ref: ipc2581::Symbol,
200225
) -> (Option<String>, Option<String>, Vec<pcb_sch::Alternative>) {
201-
// Check if AVL section exists
202-
let avl = match ipc.avl() {
203-
Some(avl) => avl,
204-
None => return (None, None, Vec::new()),
226+
let Some(avl) = ipc.avl() else {
227+
return (None, None, Vec::new());
205228
};
206229

207-
// Get the OEM design number string to match against
208230
let oem_design_number_str = ipc.resolve(oem_design_number_ref);
209-
210-
// Find matching AvlItem
211-
let avl_item = match avl
231+
let Some(avl_item) = avl
212232
.items
213233
.iter()
214234
.find(|item| ipc.resolve(item.oem_design_number) == oem_design_number_str)
215-
{
216-
Some(item) => item,
217-
None => return (None, None, Vec::new()),
235+
else {
236+
return (None, None, Vec::new());
218237
};
219238

220239
if avl_item.vmpn_list.is_empty() {
@@ -237,17 +256,14 @@ pub fn lookup_from_avl(
237256
});
238257

239258
// Rest are alternatives
240-
let alternatives: Vec<pcb_sch::Alternative> = sorted_vmpn[1..]
259+
let alternatives = sorted_vmpn[1..]
241260
.iter()
242261
.filter_map(|vmpn| {
243-
let mpn = vmpn.mpns.first()?.name;
244-
let manufacturer_ref = vmpn.vendors.first()?.enterprise_ref;
245-
let manufacturer = ipc.resolve_enterprise(manufacturer_ref)?;
246-
247-
Some(pcb_sch::Alternative {
248-
mpn: ipc.resolve(mpn).to_string(),
249-
manufacturer: manufacturer.to_string(),
250-
})
262+
let mpn = ipc.resolve(vmpn.mpns.first()?.name).to_string();
263+
let manufacturer = ipc
264+
.resolve_enterprise(vmpn.vendors.first()?.enterprise_ref)?
265+
.to_string();
266+
Some(pcb_sch::Alternative { mpn, manufacturer })
251267
})
252268
.collect();
253269

0 commit comments

Comments
 (0)