Skip to main content

altium_format/query/
selector.rs

1//! Selector AST types for querying schematic records.
2//!
3//! The selector syntax provides ergonomic shortcuts for common queries:
4//!
5//! | Pattern | Meaning |
6//! |---------|---------|
7//! | `U1` | Component by designator |
8//! | `R*` | Components matching pattern |
9//! | `U1:3` | Pin by number |
10//! | `U1:VCC` | Pin by name |
11//! | `$LM358` | Component by part number |
12//! | `~VCC` | Net by name |
13//! | `@10K` | Component by value |
14//! | `#Power` | Sheet by name |
15
16use super::common::FilterOp;
17use super::pattern::Pattern;
18
19/// A parsed selector, which may contain multiple alternatives (comma-separated).
20#[derive(Debug, Clone)]
21pub struct Selector {
22    /// Union of selector chains (comma-separated alternatives).
23    pub alternatives: Vec<SelectorChain>,
24}
25
26impl Selector {
27    /// Create a selector with a single chain.
28    pub fn single(chain: SelectorChain) -> Self {
29        Self {
30            alternatives: vec![chain],
31        }
32    }
33
34    /// Create a selector matching any record.
35    pub fn any() -> Self {
36        Self::single(SelectorChain::single(SelectorSegment::any()))
37    }
38
39    /// Returns true if this selector has no segments.
40    pub fn is_empty(&self) -> bool {
41        self.alternatives.is_empty() || self.alternatives.iter().all(|c| c.segments.is_empty())
42    }
43}
44
45/// A chain of selector segments connected by combinators.
46#[derive(Debug, Clone)]
47pub struct SelectorChain {
48    /// Segments in this chain, each connected to the next by its combinator.
49    pub segments: Vec<SelectorSegment>,
50}
51
52impl SelectorChain {
53    /// Create a chain with a single segment.
54    pub fn single(segment: SelectorSegment) -> Self {
55        Self {
56            segments: vec![segment],
57        }
58    }
59
60    /// Create an empty chain.
61    pub fn empty() -> Self {
62        Self { segments: vec![] }
63    }
64
65    /// Add a segment to this chain.
66    pub fn push(&mut self, segment: SelectorSegment) {
67        self.segments.push(segment);
68    }
69}
70
71/// A single segment in a selector chain.
72#[derive(Debug, Clone)]
73pub struct SelectorSegment {
74    /// What type of record to match.
75    pub matcher: RecordMatcher,
76    /// Property filters `[prop op value]`.
77    pub filters: Vec<PropertyFilter>,
78    /// Pseudo-selectors like `:connected`, `:has()`.
79    pub pseudo: Vec<PseudoSelector>,
80    /// How this segment connects to the next (if any).
81    pub combinator: Option<Combinator>,
82}
83
84impl SelectorSegment {
85    /// Create a segment matching any record.
86    pub fn any() -> Self {
87        Self {
88            matcher: RecordMatcher::Any,
89            filters: vec![],
90            pseudo: vec![],
91            combinator: None,
92        }
93    }
94
95    /// Create a segment from a matcher.
96    pub fn from_matcher(matcher: RecordMatcher) -> Self {
97        Self {
98            matcher,
99            filters: vec![],
100            pseudo: vec![],
101            combinator: None,
102        }
103    }
104
105    /// Add a property filter to this segment.
106    pub fn with_filter(mut self, filter: PropertyFilter) -> Self {
107        self.filters.push(filter);
108        self
109    }
110
111    /// Add a pseudo-selector to this segment.
112    pub fn with_pseudo(mut self, pseudo: PseudoSelector) -> Self {
113        self.pseudo.push(pseudo);
114        self
115    }
116
117    /// Set the combinator for this segment.
118    pub fn with_combinator(mut self, combinator: Combinator) -> Self {
119        self.combinator = Some(combinator);
120        self
121    }
122}
123
124/// What records a segment matches.
125#[derive(Debug, Clone)]
126pub enum RecordMatcher {
127    /// Match any record type: `*`
128    Any,
129
130    /// Match specific record type: `component`, `pin`, `wire`
131    Type(RecordType),
132
133    /// Match by designator pattern: `U1`, `R*`, `C??`
134    Designator(Pattern),
135
136    /// Match by part number / library reference: `$LM358`, `$STM32*`
137    PartNumber(Pattern),
138
139    /// Match by net name: `~VCC`, `~SPI_*`
140    Net(Pattern),
141
142    /// Match by value parameter: `@10K`, `@100nF`
143    Value(Pattern),
144
145    /// Match by sheet name: `#Power`, `#Analog`
146    Sheet(Pattern),
147
148    /// Pin access: `U1:3`, `U1:VCC`, `R*:1`
149    Pin {
150        /// Component designator pattern
151        component: Pattern,
152        /// Pin designator or name pattern
153        pin: Pattern,
154    },
155
156    /// Net connectivity query: `~VCC:pins`, `~GND:components`
157    NetConnected {
158        /// Net name pattern
159        net: Pattern,
160        /// What to return
161        target: NetConnectedTarget,
162    },
163
164    /// Combined designator + value: `R*@10K`
165    DesignatorWithValue {
166        /// Designator pattern
167        designator: Pattern,
168        /// Value pattern
169        value: Pattern,
170    },
171}
172
173/// Target for net connectivity queries.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum NetConnectedTarget {
176    /// Return pins connected to the net
177    Pins,
178    /// Return components connected to the net
179    Components,
180}
181
182/// Record types that can be matched.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
184pub enum RecordType {
185    Component,
186    Pin,
187    Wire,
188    NetLabel,
189    Port,
190    PowerObject,
191    Junction,
192    Label,
193    Rectangle,
194    Line,
195    Arc,
196    Ellipse,
197    Polygon,
198    Polyline,
199    Bezier,
200    Image,
201    Parameter,
202    Sheet,
203    Symbol,
204    Designator,
205    TextFrame,
206}
207
208impl RecordType {
209    /// Parse a record type from a string.
210    pub fn try_parse(s: &str) -> Option<Self> {
211        match s.to_lowercase().as_str() {
212            "component" => Some(Self::Component),
213            "pin" => Some(Self::Pin),
214            "wire" => Some(Self::Wire),
215            "netlabel" | "net_label" => Some(Self::NetLabel),
216            "port" => Some(Self::Port),
217            "power" | "powerobject" | "power_object" => Some(Self::PowerObject),
218            "junction" => Some(Self::Junction),
219            "label" => Some(Self::Label),
220            "rectangle" | "rect" => Some(Self::Rectangle),
221            "line" => Some(Self::Line),
222            "arc" => Some(Self::Arc),
223            "ellipse" => Some(Self::Ellipse),
224            "polygon" => Some(Self::Polygon),
225            "polyline" => Some(Self::Polyline),
226            "bezier" => Some(Self::Bezier),
227            "image" => Some(Self::Image),
228            "parameter" | "param" => Some(Self::Parameter),
229            "sheet" => Some(Self::Sheet),
230            "symbol" => Some(Self::Symbol),
231            "designator" => Some(Self::Designator),
232            "textframe" | "text_frame" => Some(Self::TextFrame),
233            _ => None,
234        }
235    }
236
237    /// Get the string name of this record type.
238    pub fn as_str(&self) -> &'static str {
239        match self {
240            Self::Component => "component",
241            Self::Pin => "pin",
242            Self::Wire => "wire",
243            Self::NetLabel => "netlabel",
244            Self::Port => "port",
245            Self::PowerObject => "power",
246            Self::Junction => "junction",
247            Self::Label => "label",
248            Self::Rectangle => "rectangle",
249            Self::Line => "line",
250            Self::Arc => "arc",
251            Self::Ellipse => "ellipse",
252            Self::Polygon => "polygon",
253            Self::Polyline => "polyline",
254            Self::Bezier => "bezier",
255            Self::Image => "image",
256            Self::Parameter => "parameter",
257            Self::Sheet => "sheet",
258            Self::Symbol => "symbol",
259            Self::Designator => "designator",
260            Self::TextFrame => "textframe",
261        }
262    }
263}
264
265/// A property filter like `[rotation=90]` or `[x>1000]`.
266#[derive(Debug, Clone)]
267pub struct PropertyFilter {
268    /// Property name (case-insensitive)
269    pub property: String,
270    /// Comparison operator
271    pub operator: FilterOperator,
272    /// Value to compare against
273    pub value: FilterValue,
274}
275
276impl PropertyFilter {
277    /// Create a new property filter.
278    pub fn new(property: impl Into<String>, operator: FilterOperator, value: FilterValue) -> Self {
279        Self {
280            property: property.into(),
281            operator,
282            value,
283        }
284    }
285
286    /// Create an equality filter.
287    pub fn eq(property: impl Into<String>, value: impl Into<String>) -> Self {
288        Self::new(
289            property,
290            FilterOperator::Equal,
291            FilterValue::String(value.into()),
292        )
293    }
294
295    /// Create a wildcard filter.
296    pub fn matches(property: impl Into<String>, pattern: Pattern) -> Self {
297        Self::new(
298            property,
299            FilterOperator::Wildcard,
300            FilterValue::Pattern(pattern),
301        )
302    }
303}
304
305/// Comparison operators for property filters.
306#[derive(Debug, Clone, Copy, PartialEq, Eq)]
307pub enum FilterOperator {
308    /// Exact match: `=`
309    Equal,
310    /// Not equal: `!=`
311    NotEqual,
312    /// Wildcard/pattern match: `~=`
313    Wildcard,
314    /// Starts with: `^=`
315    StartsWith,
316    /// Ends with: `$=`
317    EndsWith,
318    /// Contains: `*=`
319    Contains,
320    /// Greater than (numeric): `>`
321    GreaterThan,
322    /// Less than (numeric): `<`
323    LessThan,
324    /// Greater or equal (numeric): `>=`
325    GreaterOrEqual,
326    /// Less or equal (numeric): `<=`
327    LessOrEqual,
328}
329
330impl FilterOperator {
331    /// Parse an operator from its string representation.
332    pub fn try_parse(s: &str) -> Option<Self> {
333        match s {
334            "=" => Some(Self::Equal),
335            "!=" => Some(Self::NotEqual),
336            "~=" => Some(Self::Wildcard),
337            "^=" => Some(Self::StartsWith),
338            "$=" => Some(Self::EndsWith),
339            "*=" => Some(Self::Contains),
340            ">" => Some(Self::GreaterThan),
341            "<" => Some(Self::LessThan),
342            ">=" => Some(Self::GreaterOrEqual),
343            "<=" => Some(Self::LessOrEqual),
344            _ => None,
345        }
346    }
347
348    /// Convert to shared FilterOp type.
349    pub fn to_filter_op(&self) -> FilterOp {
350        match self {
351            Self::Equal => FilterOp::Equals,
352            Self::NotEqual => FilterOp::NotEquals,
353            Self::Wildcard => FilterOp::WordMatch, // Closest equivalent
354            Self::StartsWith => FilterOp::StartsWith,
355            Self::EndsWith => FilterOp::EndsWith,
356            Self::Contains => FilterOp::Contains,
357            Self::GreaterThan => FilterOp::GreaterThan,
358            Self::LessThan => FilterOp::LessThan,
359            Self::GreaterOrEqual => FilterOp::GreaterOrEqual,
360            Self::LessOrEqual => FilterOp::LessOrEqual,
361        }
362    }
363
364    /// Create from shared FilterOp type.
365    pub fn from_filter_op(op: FilterOp) -> Self {
366        match op {
367            FilterOp::Exists => Self::Equal, // No direct equivalent, use equal
368            FilterOp::Equals => Self::Equal,
369            FilterOp::NotEquals => Self::NotEqual,
370            FilterOp::WordMatch => Self::Wildcard,
371            FilterOp::StartsWith => Self::StartsWith,
372            FilterOp::EndsWith => Self::EndsWith,
373            FilterOp::Contains => Self::Contains,
374            FilterOp::GreaterThan => Self::GreaterThan,
375            FilterOp::LessThan => Self::LessThan,
376            FilterOp::GreaterOrEqual => Self::GreaterOrEqual,
377            FilterOp::LessOrEqual => Self::LessOrEqual,
378        }
379    }
380}
381
382/// Value in a property filter.
383#[derive(Debug, Clone)]
384pub enum FilterValue {
385    /// String value
386    String(String),
387    /// Numeric value
388    Number(f64),
389    /// Boolean value
390    Bool(bool),
391    /// Pattern for wildcard matching
392    Pattern(Pattern),
393}
394
395/// Pseudo-selectors for additional filtering.
396#[derive(Debug, Clone)]
397pub enum PseudoSelector {
398    // === Hierarchy ===
399    /// Top-level records with no parent
400    Root,
401    /// Records with no children
402    Empty,
403    /// First child of parent
404    FirstChild,
405    /// Last child of parent
406    LastChild,
407    /// Nth child (1-indexed)
408    NthChild(usize),
409    /// Only child of parent
410    OnlyChild,
411
412    // === Electrical state (for pins) ===
413    /// Connected to a net
414    Connected,
415    /// Not connected to any net
416    Unconnected,
417    /// Input pin
418    Input,
419    /// Output pin
420    Output,
421    /// Bidirectional pin
422    Bidirectional,
423    /// Power pin
424    Power,
425    /// Passive pin
426    Passive,
427    /// Open collector pin
428    OpenCollector,
429    /// Open emitter pin
430    OpenEmitter,
431    /// High-Z pin
432    HiZ,
433
434    // === Visibility ===
435    /// Not hidden
436    Visible,
437    /// Hidden records
438    Hidden,
439    /// Currently selected (UI state)
440    Selected,
441
442    // === Combinatorial ===
443    /// Does not match the given selector
444    Not(Box<Selector>),
445    /// Contains a descendant matching the given selector
446    Has(Box<Selector>),
447    /// Matches any of the given selectors (grouping)
448    Is(Box<Selector>),
449}
450
451impl PseudoSelector {
452    /// Parse a pseudo-selector from its name.
453    pub fn from_name(name: &str) -> Option<Self> {
454        match name.to_lowercase().as_str() {
455            "root" => Some(Self::Root),
456            "empty" => Some(Self::Empty),
457            "first-child" | "firstchild" => Some(Self::FirstChild),
458            "last-child" | "lastchild" => Some(Self::LastChild),
459            "only-child" | "onlychild" => Some(Self::OnlyChild),
460            "connected" => Some(Self::Connected),
461            "unconnected" => Some(Self::Unconnected),
462            "input" => Some(Self::Input),
463            "output" => Some(Self::Output),
464            "bidirectional" | "bidir" => Some(Self::Bidirectional),
465            "power" => Some(Self::Power),
466            "passive" => Some(Self::Passive),
467            "open-collector" | "opencollector" | "oc" => Some(Self::OpenCollector),
468            "open-emitter" | "openemitter" | "oe" => Some(Self::OpenEmitter),
469            "hiz" | "hi-z" | "high-z" => Some(Self::HiZ),
470            "visible" => Some(Self::Visible),
471            "hidden" => Some(Self::Hidden),
472            "selected" => Some(Self::Selected),
473            _ => None,
474        }
475    }
476}
477
478/// Combinators connecting selector segments.
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum Combinator {
481    /// Descendant (any depth): ` ` (space)
482    Descendant,
483    /// Direct child only: `>` or `/`
484    DirectChild,
485}