atoxide_parts/lib.rs
1//! Part database interface for the Ato electronics compiler.
2//!
3//! This crate provides traits and types for querying electronic component
4//! databases like JLCPCB/LCSC, Digikey, and others. It enables part selection
5//! based on solved parameter constraints from the constraint solver.
6//!
7//! # Overview
8//!
9//! The part selection process works as follows:
10//!
11//! 1. The constraint solver produces parameter bounds (e.g., resistance within 9.5k-10.5k ohm)
12//! 2. A [`PartQuery`] is constructed from these constraints
13//! 3. A [`PartDatabase`] implementation queries a parts API or local database
14//! 4. A [`PartSelector`] scores and ranks the matching parts
15//! 5. The best matching part is selected
16//!
17//! # Main Types
18//!
19//! - [`Part`] - A component part with parameters, package, and availability info
20//! - [`PartId`] - Unique identifier for a part (supplier + part number)
21//! - [`PartQuery`] - Query criteria for finding matching parts
22//! - [`PartDatabase`] - Trait for querying part databases
23//! - [`PartSelector`] - Trait for ranking/selecting parts
24//! - [`LcscClient`] - LCSC/JLCPCB API client implementation
25//! - [`PartCache`] - SQLite cache for offline-first operation
26//!
27//! # Example
28//!
29//! ```
30//! use atoxide_parts::{PartQuery, ResistorQuery, ParameterConstraint, ComponentType};
31//!
32//! // Create a query for a 10k resistor in 0402 package
33//! let query = ResistorQuery::new()
34//! .resistance_range(9500.0, 10500.0) // 10k +/- 5%
35//! .package("0402")
36//! .power(0.0625) // 1/16W minimum
37//! .tolerance(1.0) // 1% or better
38//! .build();
39//!
40//! // Or build a query directly
41//! let query = PartQuery::resistor()
42//! .with_package("0402")
43//! .with_resistance(ParameterConstraint::between(9500.0, 10500.0))
44//! .with_min_stock(100)
45//! .with_limit(10);
46//! ```
47//!
48//! # Database Implementations
49//!
50//! - **LCSC**: The [`LcscClient`] connects to the LCSC/JLCPCB component database
51//! - **Cached**: The [`CachedDatabase`] wraps any database with SQLite caching
52
53mod cache;
54mod database;
55mod lcsc;
56mod part;
57mod query;
58
59pub use part::{
60 Availability, Manufacturer, Package, ParameterValue, Part, PartClass, PartId, PartParameters,
61 PriceTier,
62};
63
64pub use database::{
65 BasicPartSelector, DatabaseError, DatabaseResult, PartDatabase, PartSelection, PartSelector,
66 SelectionConfig, SelectionStrategy,
67};
68
69pub use query::{CapacitorQuery, ComponentType, ParameterConstraint, PartQuery, ResistorQuery};
70
71pub use lcsc::LcscClient;
72
73pub use cache::{CachedDatabase, PartCache};
74
75/// Prelude module for convenient imports.
76pub mod prelude {
77 pub use crate::{
78 CachedDatabase, CapacitorQuery, ComponentType, LcscClient, ParameterConstraint, Part,
79 PartCache, PartDatabase, PartId, PartQuery, PartSelector, ResistorQuery,
80 };
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn test_integration() {
89 // Create a part
90 let part = Part::new(
91 PartId::lcsc(25871),
92 Manufacturer::new("Yageo", "RC0402FR-0710KL"),
93 Package::new("0402"),
94 "10kOhm 1% 1/16W 0402 Resistor",
95 )
96 .with_parameters(
97 PartParameters::new()
98 .with_param_unit(
99 "resistance",
100 ParameterValue::scalar(10000.0),
101 atoxide_domain::Unit::Ohm,
102 )
103 .with_param("tolerance", ParameterValue::scalar(0.01)),
104 )
105 .with_availability(Availability::with_stock(50000));
106
107 // Create a query that matches
108 let query = ResistorQuery::new()
109 .resistance_range(9500.0, 10500.0)
110 .package("0402")
111 .build();
112
113 // Verify the part would match
114 assert_eq!(query.component_type, Some(ComponentType::Resistor));
115 assert_eq!(query.package, Some("0402".to_string()));
116
117 // Verify part parameters
118 let resistance = part.get_param("resistance").unwrap().as_scalar().unwrap();
119 let constraint = query.parameters.get("resistance").unwrap();
120 assert!(constraint.is_satisfied_by(resistance));
121 }
122
123 #[test]
124 fn test_selector() {
125 let selector = BasicPartSelector::new();
126 let config = SelectionConfig::new()
127 .with_strategy(SelectionStrategy::Cheapest)
128 .with_min_stock(100);
129
130 // Create some test parts
131 let parts = vec![
132 Part::new(
133 PartId::lcsc(1),
134 Manufacturer::new("Test", "P1"),
135 Package::new("0402"),
136 "Part 1",
137 )
138 .with_availability({
139 let mut a = Availability::with_stock(1000);
140 a.part_class = PartClass::Basic;
141 a
142 }),
143 Part::new(
144 PartId::lcsc(2),
145 Manufacturer::new("Test", "P2"),
146 Package::new("0402"),
147 "Part 2",
148 )
149 .with_availability({
150 let mut a = Availability::with_stock(500);
151 a.part_class = PartClass::Extended;
152 a
153 }),
154 ];
155
156 let selections = selector.select(parts, &config);
157 assert_eq!(selections.len(), 2);
158
159 // Basic part should be ranked higher
160 assert_eq!(selections[0].part.id.supplier_id, "C1");
161 }
162}