Skip to main content

atoxide_parts/
query.rs

1//! Query builder for part database searches.
2//!
3//! This module provides a fluent API for building part queries that can
4//! filter by component type, parameters, package, and other criteria.
5
6use atoxide_domain::QuantityInterval;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// The type of component being searched for.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum ComponentType {
13    /// Resistor.
14    Resistor,
15    /// Capacitor.
16    Capacitor,
17    /// Inductor.
18    Inductor,
19    /// Diode (including LEDs).
20    Diode,
21    /// LED.
22    Led,
23    /// Transistor (BJT).
24    Transistor,
25    /// MOSFET.
26    Mosfet,
27    /// Integrated circuit.
28    IntegratedCircuit,
29    /// Connector.
30    Connector,
31    /// Crystal/oscillator.
32    Crystal,
33    /// Fuse.
34    Fuse,
35    /// Other/unknown.
36    Other,
37}
38
39impl ComponentType {
40    /// Get the API endpoint name for this component type.
41    pub fn endpoint_name(&self) -> &'static str {
42        match self {
43            ComponentType::Resistor => "resistors",
44            ComponentType::Capacitor => "capacitors",
45            ComponentType::Inductor => "inductors",
46            ComponentType::Diode => "diodes",
47            ComponentType::Led => "leds",
48            ComponentType::Transistor => "transistors",
49            ComponentType::Mosfet => "mosfets",
50            ComponentType::IntegratedCircuit => "ics",
51            ComponentType::Connector => "connectors",
52            ComponentType::Crystal => "crystals",
53            ComponentType::Fuse => "fuses",
54            ComponentType::Other => "other",
55        }
56    }
57}
58
59/// A constraint on a parameter value.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub enum ParameterConstraint {
62    /// Exact value (with small tolerance for floating point).
63    Equals(f64),
64    /// Minimum value (inclusive).
65    GreaterOrEqual(f64),
66    /// Maximum value (inclusive).
67    LessOrEqual(f64),
68    /// Within a range (inclusive on both ends).
69    Range { min: f64, max: f64 },
70    /// Within an interval with units.
71    Interval(QuantityInterval),
72}
73
74impl ParameterConstraint {
75    /// Create an exact match constraint.
76    pub fn equals(value: f64) -> Self {
77        Self::Equals(value)
78    }
79
80    /// Create a minimum constraint.
81    pub fn at_least(value: f64) -> Self {
82        Self::GreaterOrEqual(value)
83    }
84
85    /// Create a maximum constraint.
86    pub fn at_most(value: f64) -> Self {
87        Self::LessOrEqual(value)
88    }
89
90    /// Create a range constraint.
91    pub fn between(min: f64, max: f64) -> Self {
92        Self::Range { min, max }
93    }
94
95    /// Create a constraint from a quantity interval.
96    pub fn interval(interval: QuantityInterval) -> Self {
97        Self::Interval(interval)
98    }
99
100    /// Check if a value satisfies this constraint.
101    pub fn is_satisfied_by(&self, value: f64) -> bool {
102        match self {
103            Self::Equals(v) => (value - v).abs() < v.abs() * 1e-9 + 1e-15,
104            Self::GreaterOrEqual(min) => value >= *min,
105            Self::LessOrEqual(max) => value <= *max,
106            Self::Range { min, max } => value >= *min && value <= *max,
107            Self::Interval(interval) => {
108                value >= interval.min().value() && value <= interval.max().value()
109            }
110        }
111    }
112
113    /// Get the minimum value of this constraint (if defined).
114    pub fn min_value(&self) -> Option<f64> {
115        match self {
116            Self::Equals(v) => Some(*v),
117            Self::GreaterOrEqual(min) => Some(*min),
118            Self::LessOrEqual(_) => None,
119            Self::Range { min, .. } => Some(*min),
120            Self::Interval(interval) => Some(interval.min().value()),
121        }
122    }
123
124    /// Get the maximum value of this constraint (if defined).
125    pub fn max_value(&self) -> Option<f64> {
126        match self {
127            Self::Equals(v) => Some(*v),
128            Self::GreaterOrEqual(_) => None,
129            Self::LessOrEqual(max) => Some(*max),
130            Self::Range { max, .. } => Some(*max),
131            Self::Interval(interval) => Some(interval.max().value()),
132        }
133    }
134}
135
136/// A query for finding parts in a database.
137#[derive(Debug, Clone, Default, Serialize, Deserialize)]
138pub struct PartQuery {
139    /// The component type to search for.
140    pub component_type: Option<ComponentType>,
141    /// Package filter (e.g., "0402", "0603").
142    pub package: Option<String>,
143    /// Parameter constraints.
144    pub parameters: HashMap<String, ParameterConstraint>,
145    /// Minimum stock required.
146    pub min_stock: Option<u32>,
147    /// Only include basic/preferred parts.
148    pub basic_only: bool,
149    /// Maximum number of results.
150    pub limit: Option<usize>,
151}
152
153impl PartQuery {
154    /// Create a new empty query.
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Create a query for a specific component type.
160    pub fn for_type(component_type: ComponentType) -> Self {
161        Self {
162            component_type: Some(component_type),
163            ..Default::default()
164        }
165    }
166
167    /// Create a query for resistors.
168    pub fn resistor() -> Self {
169        Self::for_type(ComponentType::Resistor)
170    }
171
172    /// Create a query for capacitors.
173    pub fn capacitor() -> Self {
174        Self::for_type(ComponentType::Capacitor)
175    }
176
177    /// Create a query for inductors.
178    pub fn inductor() -> Self {
179        Self::for_type(ComponentType::Inductor)
180    }
181
182    /// Set the package filter.
183    pub fn with_package(mut self, package: impl Into<String>) -> Self {
184        self.package = Some(package.into());
185        self
186    }
187
188    /// Add a parameter constraint.
189    pub fn with_param(mut self, name: impl Into<String>, constraint: ParameterConstraint) -> Self {
190        self.parameters.insert(name.into(), constraint);
191        self
192    }
193
194    /// Add a resistance constraint (for resistors).
195    pub fn with_resistance(self, constraint: ParameterConstraint) -> Self {
196        self.with_param("resistance", constraint)
197    }
198
199    /// Add a capacitance constraint (for capacitors).
200    pub fn with_capacitance(self, constraint: ParameterConstraint) -> Self {
201        self.with_param("capacitance", constraint)
202    }
203
204    /// Add an inductance constraint (for inductors).
205    pub fn with_inductance(self, constraint: ParameterConstraint) -> Self {
206        self.with_param("inductance", constraint)
207    }
208
209    /// Add a voltage rating constraint.
210    pub fn with_voltage(self, constraint: ParameterConstraint) -> Self {
211        self.with_param("voltage", constraint)
212    }
213
214    /// Add a power rating constraint.
215    pub fn with_power(self, constraint: ParameterConstraint) -> Self {
216        self.with_param("power", constraint)
217    }
218
219    /// Add a tolerance constraint.
220    pub fn with_tolerance(self, constraint: ParameterConstraint) -> Self {
221        self.with_param("tolerance", constraint)
222    }
223
224    /// Set minimum stock.
225    pub fn with_min_stock(mut self, stock: u32) -> Self {
226        self.min_stock = Some(stock);
227        self
228    }
229
230    /// Only include basic/preferred parts.
231    pub fn basic_only(mut self) -> Self {
232        self.basic_only = true;
233        self
234    }
235
236    /// Set result limit.
237    pub fn with_limit(mut self, limit: usize) -> Self {
238        self.limit = Some(limit);
239        self
240    }
241}
242
243/// Builder for creating resistor queries with common parameters.
244#[derive(Debug, Clone, Default)]
245pub struct ResistorQuery {
246    query: PartQuery,
247}
248
249impl ResistorQuery {
250    /// Create a new resistor query.
251    pub fn new() -> Self {
252        Self {
253            query: PartQuery::resistor(),
254        }
255    }
256
257    /// Set the resistance value (exact match with tolerance).
258    pub fn resistance(mut self, ohms: f64) -> Self {
259        self.query = self
260            .query
261            .with_resistance(ParameterConstraint::equals(ohms));
262        self
263    }
264
265    /// Set a resistance range.
266    pub fn resistance_range(mut self, min_ohms: f64, max_ohms: f64) -> Self {
267        self.query = self
268            .query
269            .with_resistance(ParameterConstraint::between(min_ohms, max_ohms));
270        self
271    }
272
273    /// Set the package.
274    pub fn package(mut self, package: impl Into<String>) -> Self {
275        self.query = self.query.with_package(package);
276        self
277    }
278
279    /// Set the power rating (minimum).
280    pub fn power(mut self, watts: f64) -> Self {
281        self.query = self.query.with_power(ParameterConstraint::at_least(watts));
282        self
283    }
284
285    /// Set the tolerance (maximum).
286    pub fn tolerance(mut self, percent: f64) -> Self {
287        self.query = self
288            .query
289            .with_tolerance(ParameterConstraint::at_most(percent / 100.0));
290        self
291    }
292
293    /// Build the query.
294    pub fn build(self) -> PartQuery {
295        self.query
296    }
297}
298
299/// Builder for creating capacitor queries with common parameters.
300#[derive(Debug, Clone, Default)]
301pub struct CapacitorQuery {
302    query: PartQuery,
303}
304
305impl CapacitorQuery {
306    /// Create a new capacitor query.
307    pub fn new() -> Self {
308        Self {
309            query: PartQuery::capacitor(),
310        }
311    }
312
313    /// Set the capacitance value (exact match with tolerance).
314    pub fn capacitance(mut self, farads: f64) -> Self {
315        self.query = self
316            .query
317            .with_capacitance(ParameterConstraint::equals(farads));
318        self
319    }
320
321    /// Set a capacitance range.
322    pub fn capacitance_range(mut self, min_farads: f64, max_farads: f64) -> Self {
323        self.query = self
324            .query
325            .with_capacitance(ParameterConstraint::between(min_farads, max_farads));
326        self
327    }
328
329    /// Set the package.
330    pub fn package(mut self, package: impl Into<String>) -> Self {
331        self.query = self.query.with_package(package);
332        self
333    }
334
335    /// Set the voltage rating (minimum).
336    pub fn voltage(mut self, volts: f64) -> Self {
337        self.query = self
338            .query
339            .with_voltage(ParameterConstraint::at_least(volts));
340        self
341    }
342
343    /// Build the query.
344    pub fn build(self) -> PartQuery {
345        self.query
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_parameter_constraint() {
355        let eq = ParameterConstraint::equals(10000.0);
356        assert!(eq.is_satisfied_by(10000.0));
357        assert!(!eq.is_satisfied_by(10001.0));
358
359        let range = ParameterConstraint::between(9500.0, 10500.0);
360        assert!(range.is_satisfied_by(10000.0));
361        assert!(range.is_satisfied_by(9500.0));
362        assert!(range.is_satisfied_by(10500.0));
363        assert!(!range.is_satisfied_by(9499.0));
364        assert!(!range.is_satisfied_by(10501.0));
365
366        let at_least = ParameterConstraint::at_least(5.0);
367        assert!(at_least.is_satisfied_by(5.0));
368        assert!(at_least.is_satisfied_by(100.0));
369        assert!(!at_least.is_satisfied_by(4.9));
370    }
371
372    #[test]
373    fn test_part_query() {
374        let query = PartQuery::resistor()
375            .with_package("0402")
376            .with_resistance(ParameterConstraint::between(9500.0, 10500.0))
377            .with_power(ParameterConstraint::at_least(0.0625))
378            .with_min_stock(100)
379            .with_limit(10);
380
381        assert_eq!(query.component_type, Some(ComponentType::Resistor));
382        assert_eq!(query.package, Some("0402".to_string()));
383        assert!(query.parameters.contains_key("resistance"));
384        assert!(query.parameters.contains_key("power"));
385        assert_eq!(query.min_stock, Some(100));
386        assert_eq!(query.limit, Some(10));
387    }
388
389    #[test]
390    fn test_resistor_query_builder() {
391        let query = ResistorQuery::new()
392            .resistance_range(9500.0, 10500.0)
393            .package("0402")
394            .power(0.0625)
395            .tolerance(1.0)
396            .build();
397
398        assert_eq!(query.component_type, Some(ComponentType::Resistor));
399        assert_eq!(query.package, Some("0402".to_string()));
400        assert!(query.parameters.contains_key("resistance"));
401        assert!(query.parameters.contains_key("power"));
402        assert!(query.parameters.contains_key("tolerance"));
403    }
404
405    #[test]
406    fn test_capacitor_query_builder() {
407        let query = CapacitorQuery::new()
408            .capacitance(100e-9) // 100nF
409            .package("0603")
410            .voltage(16.0)
411            .build();
412
413        assert_eq!(query.component_type, Some(ComponentType::Capacitor));
414        assert_eq!(query.package, Some("0603".to_string()));
415        assert!(query.parameters.contains_key("capacitance"));
416        assert!(query.parameters.contains_key("voltage"));
417    }
418
419    #[test]
420    fn test_component_type() {
421        assert_eq!(ComponentType::Resistor.endpoint_name(), "resistors");
422        assert_eq!(ComponentType::Capacitor.endpoint_name(), "capacitors");
423        assert_eq!(ComponentType::Mosfet.endpoint_name(), "mosfets");
424    }
425}