Skip to main content

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}