use crate::datasheets::schema::{DatasheetDatabase, DatasheetRequirements};
use crate::parser::schema::Component;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MatcherError {
#[error("Failed to read datasheet file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse datasheet JSON: {0}")]
ParseError(#[from] serde_json::Error),
#[error("Invalid datasheet directory: {0}")]
InvalidDirectory(String),
}
pub struct DatasheetMatcher {
database: DatasheetDatabase,
}
impl DatasheetMatcher {
pub fn new() -> Self {
Self {
database: DatasheetDatabase::new(),
}
}
pub fn with_builtin_datasheets() -> Self {
let mut matcher = Self::new();
matcher.load_all_datasheets();
matcher
}
pub fn load_all_datasheets(&mut self) {
let datasheets = crate::datasheets::builtin::load_all_datasheets();
for ds in datasheets {
self.database.add(ds);
}
tracing::info!("Loaded {} total datasheets", self.database.count());
}
pub fn load_builtin_datasheets(&mut self) {
let datasheets = crate::datasheets::builtin::get_all_datasheets();
for ds in datasheets {
self.database.add(ds);
}
tracing::info!("Loaded {} embedded datasheets", self.database.count());
}
pub fn load_from_directory(&mut self, dir: &Path) -> Result<usize, MatcherError> {
if !dir.is_dir() {
return Err(MatcherError::InvalidDirectory(
dir.to_string_lossy().to_string()
));
}
let mut count = 0;
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "json").unwrap_or(false) {
match self.load_from_file(&path) {
Ok(_) => count += 1,
Err(e) => {
tracing::warn!("Failed to load datasheet from {:?}: {}", path, e);
}
}
}
}
tracing::info!("Loaded {} datasheets from {:?}", count, dir);
Ok(count)
}
pub fn load_from_file(&mut self, path: &Path) -> Result<(), MatcherError> {
let content = std::fs::read_to_string(path)?;
let requirements: DatasheetRequirements = serde_json::from_str(&content)?;
self.database.add(requirements);
Ok(())
}
pub fn match_component(&self, component: &Component) -> Option<&DatasheetRequirements> {
if let Some(req) = self.database.get(&component.value) {
return Some(req);
}
if let Some(part_name) = component.lib_id.split(':').last() {
if let Some(req) = self.database.get(part_name) {
return Some(req);
}
}
if let Some(req) = self.database.get(&component.lib_id) {
return Some(req);
}
for (key, value) in &component.properties {
let key_lower = key.to_lowercase();
if key_lower.contains("part") || key_lower.contains("mpn") || key_lower == "pn" {
if let Some(req) = self.database.get(value) {
return Some(req);
}
}
}
None
}
pub fn match_all_components<'a>(
&'a self,
components: &'a [Component],
) -> Vec<(&'a Component, &'a DatasheetRequirements)> {
components
.iter()
.filter_map(|c| self.match_component(c).map(|req| (c, req)))
.collect()
}
pub fn database(&self) -> &DatasheetDatabase {
&self.database
}
pub fn datasheet_count(&self) -> usize {
self.database.count()
}
}
impl Default for DatasheetMatcher {
fn default() -> Self {
Self::with_builtin_datasheets()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use crate::parser::schema::Position;
fn create_test_component(reference: &str, value: &str, lib_id: &str) -> Component {
Component {
uuid: "test".to_string(),
reference: reference.to_string(),
value: value.to_string(),
lib_id: lib_id.to_string(),
footprint: None,
position: Position { x: 0.0, y: 0.0 },
rotation: 0.0,
properties: HashMap::new(),
pins: vec![],
}
}
#[test]
fn test_matcher_with_builtin() {
let matcher = DatasheetMatcher::with_builtin_datasheets();
assert!(matcher.datasheet_count() > 0);
}
#[test]
fn test_match_by_value() {
let matcher = DatasheetMatcher::with_builtin_datasheets();
let component = create_test_component("U1", "STM32F411CEU6", "MCU:STM32");
let result = matcher.match_component(&component);
assert!(result.is_some());
assert_eq!(result.unwrap().manufacturer, "STMicroelectronics");
}
#[test]
fn test_match_by_partial() {
let matcher = DatasheetMatcher::with_builtin_datasheets();
let component = create_test_component("U1", "NE555", "Timer:NE555");
let result = matcher.match_component(&component);
assert!(result.is_some());
}
}