Skip to main content

batuta/hf/catalog/
core.rs

1//! Core catalog operations
2//!
3//! Implements HF-QUERY-001, HF-QUERY-004, HF-QUERY-005, HF-QUERY-006
4//!
5//! ## Observability (HF-OBS-003)
6//!
7//! Key catalog operations are instrumented with tracing spans:
8//! - `hf.catalog.search` - Component search operations
9//! - `hf.catalog.by_course` - Course-filtered queries
10//! - `hf.catalog.by_category` - Category-filtered queries
11
12use std::collections::HashMap;
13use tracing::{debug, info, instrument};
14
15use super::types::{AssetType, CatalogComponent, HfComponentCategory};
16
17// ============================================================================
18// HF-QUERY-001: Catalog
19// ============================================================================
20
21/// The complete HuggingFace ecosystem catalog
22#[derive(Debug, Clone, Default)]
23pub struct HfCatalog {
24    pub(crate) components: HashMap<String, CatalogComponent>,
25}
26
27impl HfCatalog {
28    /// Create empty catalog
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Load the standard catalog with all 50+ components
34    pub fn standard() -> Self {
35        let mut catalog = Self::new();
36        catalog.register_hub_components();
37        catalog.register_deployment_components();
38        catalog.register_library_components();
39        catalog.register_training_components();
40        catalog.register_collaboration_components();
41        catalog.register_community_components();
42        catalog.register_integration_components();
43        catalog
44    }
45
46    /// Add a component to the catalog
47    pub fn add(&mut self, component: CatalogComponent) {
48        self.components.insert(component.id.clone(), component);
49    }
50
51    /// Get component by ID
52    #[instrument(name = "hf.catalog.get", skip(self), fields(found = tracing::field::Empty))]
53    pub fn get(&self, id: &str) -> Option<&CatalogComponent> {
54        let result = self.components.get(id);
55        tracing::Span::current().record("found", result.is_some());
56        if result.is_some() {
57            debug!(component_id = id, "Retrieved catalog component");
58        }
59        result
60    }
61
62    /// Get all component IDs
63    pub fn list(&self) -> Vec<&str> {
64        let mut ids: Vec<_> = self.components.keys().map(String::as_str).collect();
65        ids.sort_unstable();
66        ids
67    }
68
69    /// Get total component count
70    pub fn len(&self) -> usize {
71        self.components.len()
72    }
73
74    /// Check if catalog is empty
75    pub fn is_empty(&self) -> bool {
76        self.components.is_empty()
77    }
78
79    /// Get all components as an iterator
80    pub fn all(&self) -> impl Iterator<Item = &CatalogComponent> {
81        self.components.values()
82    }
83
84    /// Get components by tag
85    #[instrument(name = "hf.catalog.by_tag", skip(self), fields(result_count = tracing::field::Empty))]
86    pub fn by_tag(&self, tag: &str) -> Vec<&CatalogComponent> {
87        let tag_lower = tag.to_lowercase();
88        let results: Vec<_> = self
89            .components
90            .values()
91            .filter(|c| c.tags.iter().any(|t| t.to_lowercase() == tag_lower))
92            .collect();
93        tracing::Span::current().record("result_count", results.len());
94        debug!(tag = tag, count = results.len(), "Tag query completed");
95        results
96    }
97
98    /// Get components by category
99    #[instrument(name = "hf.catalog.by_category", skip(self), fields(result_count = tracing::field::Empty))]
100    pub fn by_category(&self, category: HfComponentCategory) -> Vec<&CatalogComponent> {
101        let results: Vec<_> = self.components.values().filter(|c| c.category == category).collect();
102        tracing::Span::current().record("result_count", results.len());
103        debug!(
104            category = ?category,
105            count = results.len(),
106            "Category query completed"
107        );
108        results
109    }
110
111    /// Search components by query (matches id, name, description, tags)
112    #[instrument(name = "hf.catalog.search", skip(self), fields(result_count = tracing::field::Empty))]
113    pub fn search(&self, query: &str) -> Vec<&CatalogComponent> {
114        let query_lower = query.to_lowercase();
115        let results: Vec<_> = self
116            .components
117            .values()
118            .filter(|c| {
119                c.id.to_lowercase().contains(&query_lower)
120                    || c.name.to_lowercase().contains(&query_lower)
121                    || c.description.to_lowercase().contains(&query_lower)
122                    || c.tags.iter().any(|t| t.to_lowercase().contains(&query_lower))
123            })
124            .collect();
125        tracing::Span::current().record("result_count", results.len());
126        info!(query = query, count = results.len(), "Catalog search completed");
127        results
128    }
129
130    // ========================================================================
131    // HF-QUERY-004: Course Alignment
132    // ========================================================================
133
134    /// Get components for a specific course
135    #[instrument(name = "hf.catalog.by_course", skip(self), fields(result_count = tracing::field::Empty))]
136    pub fn by_course(&self, course: u8) -> Vec<&CatalogComponent> {
137        let results: Vec<_> = self
138            .components
139            .values()
140            .filter(|c| c.courses.iter().any(|ca| ca.course == course))
141            .collect();
142        tracing::Span::current().record("result_count", results.len());
143        info!(course = course, count = results.len(), "Course query completed");
144        results
145    }
146
147    /// Get components for a specific course and week
148    #[instrument(name = "hf.catalog.by_course_week", skip(self), fields(result_count = tracing::field::Empty))]
149    pub fn by_course_week(&self, course: u8, week: u8) -> Vec<&CatalogComponent> {
150        let results: Vec<_> = self
151            .components
152            .values()
153            .filter(|c| c.courses.iter().any(|ca| ca.course == course && ca.week == week))
154            .collect();
155        tracing::Span::current().record("result_count", results.len());
156        debug!(course = course, week = week, count = results.len(), "Course-week query completed");
157        results
158    }
159
160    /// Get components by asset type (labs, videos, etc.)
161    #[instrument(name = "hf.catalog.by_asset_type", skip(self), fields(result_count = tracing::field::Empty))]
162    pub fn by_asset_type(&self, asset: AssetType) -> Vec<&CatalogComponent> {
163        let results: Vec<_> = self
164            .components
165            .values()
166            .filter(|c| c.courses.iter().any(|ca| ca.asset_types.contains(&asset)))
167            .collect();
168        tracing::Span::current().record("result_count", results.len());
169        debug!(asset = ?asset, count = results.len(), "Asset type query completed");
170        results
171    }
172
173    // ========================================================================
174    // HF-QUERY-005: Dependency Graph
175    // ========================================================================
176
177    /// Get dependencies of a component
178    #[instrument(name = "hf.catalog.deps", skip(self), fields(result_count = tracing::field::Empty))]
179    pub fn deps(&self, id: &str) -> Vec<&CatalogComponent> {
180        let results = self
181            .get(id)
182            .map(|c| {
183                c.dependencies
184                    .iter()
185                    .filter_map(|dep_id| self.components.get(dep_id))
186                    .collect::<Vec<_>>()
187            })
188            .unwrap_or_default();
189        tracing::Span::current().record("result_count", results.len());
190        debug!(component_id = id, dep_count = results.len(), "Dependency lookup completed");
191        results
192    }
193
194    /// Get reverse dependencies (what depends on this component)
195    #[instrument(name = "hf.catalog.rdeps", skip(self), fields(result_count = tracing::field::Empty))]
196    pub fn rdeps(&self, id: &str) -> Vec<&CatalogComponent> {
197        let results: Vec<_> =
198            self.components.values().filter(|c| c.dependencies.contains(&id.to_string())).collect();
199        tracing::Span::current().record("result_count", results.len());
200        debug!(
201            component_id = id,
202            rdep_count = results.len(),
203            "Reverse dependency lookup completed"
204        );
205        results
206    }
207
208    /// Check if two components are compatible (no conflicts)
209    pub fn compatible(&self, id1: &str, id2: &str) -> bool {
210        // Simple compatibility: both exist and share common deps or are unrelated
211        self.get(id1).is_some() && self.get(id2).is_some()
212    }
213
214    // ========================================================================
215    // HF-QUERY-006: Documentation Links
216    // ========================================================================
217
218    /// Get documentation URL for a component
219    pub fn docs_url(&self, id: &str) -> Option<&str> {
220        self.get(id).map(|c| c.docs_url.as_str())
221    }
222
223    /// Get API reference URL (docs + /api)
224    pub fn api_url(&self, id: &str) -> Option<String> {
225        self.docs_url(id).map(|url| format!("{}/api", url.trim_end_matches('/')))
226    }
227
228    /// Get tutorials URL
229    pub fn tutorials_url(&self, id: &str) -> Option<String> {
230        self.docs_url(id).map(|url| format!("{}/tutorials", url.trim_end_matches('/')))
231    }
232}