main
John Lauer Publish 0.2.3 6e27c4b
//! Net names the way a spec and a board can agree on them. A board built from a hierarchical
//! schematic names root-sheet nets `/GND`, `/VIN`; a spec (or a flat board) says `GND`, `VIN`.
//! `netlist check` already treats the two as one net; the same rule resolves a spec's net names
//! against the board: an exact name always wins, and a name that differs only by the leading `/`
//! matches only when exactly one board net does.

use serde_json::Value;
use std::collections::BTreeSet;

/// A net name without the root sheet's leading `/` (`/VIN` -> `VIN`). A sub-sheet path
/// (`/power/VIN`) keeps its hierarchy.
pub fn norm_net(n: &str) -> String {
    let t = n.trim();
    t.strip_prefix('/').unwrap_or(t).to_string()
}

/// The board net a spec name means: the exact name when the board has it, else the one board net
/// equal to it under `norm_net`. None when there is none, or more than one (ambiguous).
pub fn resolve<'a, I>(name: &str, board_nets: I) -> Option<String>
where
    I: IntoIterator<Item = &'a String>,
{
    let want = norm_net(name);
    let mut hits: Vec<&String> = Vec::new();
    for n in board_nets {
        if n == name {
            return Some(n.clone());
        }
        if norm_net(n) == want {
            hits.push(n);
        }
    }
    (hits.len() == 1).then(|| hits[0].clone())
}

/// Board nets close to a name the board does not have, for a refusal: the same name up to the leading
/// `/`, else the same up to case.
pub fn close<'a, I>(name: &str, board_nets: I) -> Vec<String>
where
    I: IntoIterator<Item = &'a String>,
{
    let all: Vec<&String> = board_nets.into_iter().collect();
    let want = norm_net(name);
    let same: Vec<String> = all.iter().filter(|n| n.as_str() != name && norm_net(n) == want).map(|n| n.to_string()).collect();
    if !same.is_empty() {
        return same;
    }
    all.iter().filter(|n| n.as_str() != name && norm_net(n).eq_ignore_ascii_case(&want)).map(|n| n.to_string()).collect()
}

/// Every net name a `.kicad_pcb` declares or uses (`(net 3 "/GND")`, KiCad 10's `(net "/GND")`),
/// read without parsing the whole board.
pub fn names_in_text(text: &str) -> BTreeSet<String> {
    let mut out = BTreeSet::new();
    let b = text.as_bytes();
    let mut i = 0;
    while let Some(off) = text[i..].find("(net") {
        let mut j = i + off + 4;
        i = j;
        // `(net` followed by whitespace only (not `(net_name`, `(net_class`)
        if j >= b.len() || !b[j].is_ascii_whitespace() { continue; }
        while j < b.len() && b[j].is_ascii_whitespace() { j += 1; }
        while j < b.len() && b[j].is_ascii_digit() { j += 1; }
        while j < b.len() && b[j].is_ascii_whitespace() { j += 1; }
        if j >= b.len() || b[j] != b'"' { continue; }
        j += 1;
        let mut name = String::new();
        let mut chars = text[j..].char_indices();
        let mut end = None;
        while let Some((k, c)) = chars.next() {
            match c {
                '\\' => { if let Some((_, e)) = chars.next() { name.push(e); } }
                '"' => { end = Some(j + k + 1); break; }
                _ => name.push(c),
            }
        }
        if let Some(e) = end {
            if !name.is_empty() { out.insert(name); }
            i = e;
        }
    }
    out
}

/// Rewrite the net names a spec gives (pours[].net, planes, wideNets, midNets, netClearance and loads
/// keys, switchNodes, hot.*.tabNet, thermalVias[].net, stitchVias[].net, planeUnder[].net) to the
/// board's spelling when `resolve` finds exactly one. Returns `(spec name, board name)` per rename.
pub fn align_spec(spec: &mut Value, board_nets: &BTreeSet<String>) -> Vec<(String, String)> {
    let mut renames: Vec<(String, String)> = Vec::new();
    let mut fix = |s: &str| -> Option<String> {
        let r = resolve(s, board_nets.iter())?;
        if r == s { return None; }
        if !renames.iter().any(|(a, _)| a == s) { renames.push((s.to_string(), r.clone())); }
        Some(r)
    };
    let Some(obj) = spec.as_object_mut() else { return renames };
    // a field `key` holding a net name, in each object of an array (or one object)
    let in_items = |v: Option<&mut Value>, key: &str, fix: &mut dyn FnMut(&str) -> Option<String>| {
        let Some(v) = v else { return };
        let items: Vec<&mut Value> = match v { Value::Array(a) => a.iter_mut().collect(), Value::Object(_) => vec![v], _ => vec![] };
        for it in items {
            if let Some(n) = it.get(key).and_then(|n| n.as_str()).and_then(|n| fix(n)) { it[key] = Value::String(n); }
        }
    };
    in_items(obj.get_mut("pours"), "net", &mut fix);
    in_items(obj.get_mut("thermalVias"), "net", &mut fix);
    in_items(obj.get_mut("stitchVias"), "net", &mut fix);
    in_items(obj.get_mut("planeUnder"), "net", &mut fix);
    for key in ["planes", "wideNets", "midNets", "netClearance", "loads"] {
        let Some(m) = obj.get_mut(key).and_then(|v| v.as_object_mut()) else { continue };
        let keys: Vec<String> = m.keys().cloned().collect();
        for k in keys {
            if let Some(n) = fix(&k) {
                // the board's own spelling already in the map wins over the alias
                if m.contains_key(&n) { continue; }
                let v = m.remove(&k).unwrap();
                m.insert(n, v);
            }
        }
    }
    if let Some(a) = obj.get_mut("switchNodes").and_then(|v| v.as_array_mut()) {
        for x in a.iter_mut() {
            if let Some(n) = x.as_str().and_then(|s| fix(s)) { *x = Value::String(n); }
        }
    }
    if let Some(hot) = obj.get_mut("hot").and_then(|v| v.as_object_mut()) {
        for (_, h) in hot.iter_mut() {
            if let Some(n) = h.get("tabNet").and_then(|n| n.as_str()).and_then(|n| fix(n)) { h["tabNet"] = Value::String(n); }
        }
    }
    renames
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn set(a: &[&str]) -> BTreeSet<String> { a.iter().map(|s| s.to_string()).collect() }

    #[test]
    fn hierarchical_name_matches_when_unambiguous() {
        let b = set(&["/GND", "/VIN", "Net-(U1-SW)"]);
        assert_eq!(resolve("GND", b.iter()).as_deref(), Some("/GND"));
        assert_eq!(resolve("/VIN", b.iter()).as_deref(), Some("/VIN"));
        assert_eq!(resolve("VOUT", b.iter()), None);
        // the other way round: spec `/GND`, flat board `GND`
        assert_eq!(resolve("/GND", set(&["GND"]).iter()).as_deref(), Some("GND"));
        // a sub-sheet path keeps its hierarchy
        assert_eq!(resolve("VIN", set(&["/power/VIN"]).iter()), None);
    }

    #[test]
    fn exact_match_wins_over_the_alias() {
        let b = set(&["GND", "/GND"]);
        assert_eq!(resolve("GND", b.iter()).as_deref(), Some("GND"));
        assert_eq!(resolve("/GND", b.iter()).as_deref(), Some("/GND"));
    }

    #[test]
    fn close_names_the_board_spelling() {
        let b = set(&["/GND", "/VIN"]);
        assert_eq!(close("GND", b.iter()), vec!["/GND".to_string()]);
        assert_eq!(close("gnd", b.iter()), vec!["/GND".to_string()]);
        assert!(close("VOUT", b.iter()).is_empty());
    }

    #[test]
    fn names_from_board_text() {
        let t = r#"(kicad_pcb (net 0 "") (net 1 "/GND") (net 2 "/VIN") (footprint "x" (pad "1" smd rect (net "/SW")) (zone (net 1) (net_name "/GND"))))"#;
        assert_eq!(names_in_text(t), set(&["/GND", "/VIN", "/SW"]));
    }

    #[test]
    fn spec_names_follow_the_board() {
        let mut spec = json!({
            "pours": [{"net": "GND", "outline": true}, {"net": "VOUT"}],
            "planes": {"GND": "In1.Cu"}, "wideNets": {"VIN": [1.0]}, "midNets": {"/VIN": [0.5]},
            "hot": {"U1": {"watts": 0.2, "tabNet": "GND"}}, "switchNodes": ["SW"],
            "thermalVias": [{"pad": "U1.7", "net": "GND"}], "planeUnder": {"net": "GND", "layer": "B.Cu"}
        });
        let r = align_spec(&mut spec, &set(&["/GND", "/VIN", "/SW"]));
        assert_eq!(spec["pours"][0]["net"], "/GND");
        assert_eq!(spec["pours"][1]["net"], "VOUT");
        assert_eq!(spec["planes"], json!({"/GND": "In1.Cu"}));
        assert_eq!(spec["wideNets"], json!({"/VIN": [1.0]}));
        assert_eq!(spec["midNets"], json!({"/VIN": [0.5]}));
        assert_eq!(spec["hot"]["U1"]["tabNet"], "/GND");
        assert_eq!(spec["switchNodes"], json!(["/SW"]));
        assert_eq!(spec["thermalVias"][0]["net"], "/GND");
        assert_eq!(spec["planeUnder"]["net"], "/GND");
        assert!(r.contains(&("GND".into(), "/GND".into())));
        // both spellings on the board: nothing moves
        let mut s2 = json!({"pours": [{"net": "GND"}], "hot": {"U1": {"tabNet": "GND"}}});
        assert!(align_spec(&mut s2, &set(&["GND", "/GND"])).is_empty());
        assert_eq!(s2["pours"][0]["net"], "GND");
    }
}