batuta/hf/catalog/
core.rs1use std::collections::HashMap;
13use tracing::{debug, info, instrument};
14
15use super::types::{AssetType, CatalogComponent, HfComponentCategory};
16
17#[derive(Debug, Clone, Default)]
23pub struct HfCatalog {
24 pub(crate) components: HashMap<String, CatalogComponent>,
25}
26
27impl HfCatalog {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 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 pub fn add(&mut self, component: CatalogComponent) {
48 self.components.insert(component.id.clone(), component);
49 }
50
51 #[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 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 pub fn len(&self) -> usize {
71 self.components.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
76 self.components.is_empty()
77 }
78
79 pub fn all(&self) -> impl Iterator<Item = &CatalogComponent> {
81 self.components.values()
82 }
83
84 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 pub fn compatible(&self, id1: &str, id2: &str) -> bool {
210 self.get(id1).is_some() && self.get(id2).is_some()
212 }
213
214 pub fn docs_url(&self, id: &str) -> Option<&str> {
220 self.get(id).map(|c| c.docs_url.as_str())
221 }
222
223 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 pub fn tutorials_url(&self, id: &str) -> Option<String> {
230 self.docs_url(id).map(|url| format!("{}/tutorials", url.trim_end_matches('/')))
231 }
232}