components_rs/components/
registry.rs1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::components::types::*;
5use crate::context::expand::{self, ContextResolver, ExpandedNode};
6use crate::error::{ComponentsJsError, Result};
7use crate::fs::{self as cfs, Fs};
8use crate::module_state::ModuleState;
9
10#[derive(Debug, Clone)]
12pub struct ComponentRegistry {
13 pub components: HashMap<String, CjsComponent>,
15 pub modules: HashMap<String, CjsModule>,
17}
18
19#[derive(Debug, Clone)]
21struct CollectedNode {
22 id: String,
23 types: Vec<String>,
24 properties: HashMap<String, Vec<serde_json::Value>>,
25 source_file: String,
26}
27
28impl ComponentRegistry {
29 pub fn new() -> Self {
30 Self {
31 components: HashMap::new(),
32 modules: HashMap::new(),
33 }
34 }
35
36 pub async fn register_available_modules(
39 &mut self,
40 fs: &dyn Fs,
41 state: &ModuleState,
42 ) -> Result<()> {
43 let mut all_nodes: HashMap<String, CollectedNode> = HashMap::new();
44 let mut visited_files: std::collections::HashSet<PathBuf> =
45 std::collections::HashSet::new();
46
47 for version_map in state.component_modules.values() {
49 for component_path in version_map.values() {
50 if cfs::exists(fs, component_path).await {
51 self.collect_nodes_from_file(
52 fs,
53 component_path,
54 state,
55 &mut all_nodes,
56 &mut visited_files,
57 )
58 .await?;
59 } else {
60 tracing::warn!(
61 "Component file does not exist: {}",
62 component_path.display()
63 );
64 }
65 }
66 }
67
68 tracing::info!(
69 "Collected {} unique nodes from component files",
70 all_nodes.len()
71 );
72
73 self.process_merged_nodes(&all_nodes, state)?;
75
76 Ok(())
77 }
78
79 fn collect_nodes_from_file<'a>(
81 &'a self,
82 fs: &'a dyn Fs,
83 path: &'a Path,
84 state: &'a ModuleState,
85 all_nodes: &'a mut HashMap<String, CollectedNode>,
86 visited: &'a mut std::collections::HashSet<PathBuf>,
87 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
88 Box::pin(async move {
89 let canonical = fs
90 .canonicalize(path)
91 .await
92 .unwrap_or_else(|_| path.to_path_buf());
93 if visited.contains(&canonical) {
94 return Ok(());
95 }
96 visited.insert(canonical.clone());
97
98 tracing::debug!("Loading component file: {}", path.display());
99
100 let contents = fs.read_to_string(path).await?;
101 let doc: serde_json::Value =
102 serde_json::from_str(&contents).map_err(|e| ComponentsJsError::JsonParse {
103 path: path.display().to_string(),
104 source: e,
105 })?;
106
107 let resolver = if let Some(ctx) = doc.get("@context") {
109 ContextResolver::from_context_value(ctx, &state.contexts)?
110 } else {
111 ContextResolver::new()
112 };
113
114 let nodes = expand::extract_graph_nodes(&doc, &state.contexts)?;
116 let source = path.display().to_string();
117
118 for node in &nodes {
119 if let Some(id) = &node.id {
120 let entry =
121 all_nodes
122 .entry(id.clone())
123 .or_insert_with(|| CollectedNode {
124 id: id.clone(),
125 types: Vec::new(),
126 properties: HashMap::new(),
127 source_file: source.clone(),
128 });
129 for t in &node.types {
130 if !entry.types.contains(t) {
131 entry.types.push(t.clone());
132 }
133 }
134 for (key, vals) in &node.properties {
135 entry
136 .properties
137 .entry(key.clone())
138 .or_default()
139 .extend(vals.clone());
140 }
141 }
142 }
143
144 self.process_imports_collect(fs, &doc, &nodes, &resolver, state, all_nodes, visited)
146 .await?;
147
148 Ok(())
149 })
150 }
151
152 fn process_imports_collect<'a>(
154 &'a self,
155 fs: &'a dyn Fs,
156 doc: &'a serde_json::Value,
157 nodes: &'a [ExpandedNode],
158 resolver: &'a ContextResolver,
159 state: &'a ModuleState,
160 all_nodes: &'a mut HashMap<String, CollectedNode>,
161 visited: &'a mut std::collections::HashSet<PathBuf>,
162 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
163 Box::pin(async move {
164 let mut import_iris = Vec::new();
165
166 if let Some(import_val) = doc.get("import") {
167 collect_import_iris(import_val, resolver, &mut import_iris);
168 }
169
170 for node in nodes {
171 if let Some(imports) = node.properties.get(IRI_RDFS_SEE_ALSO) {
172 for import_val in imports {
173 collect_import_iris(import_val, resolver, &mut import_iris);
174 }
175 }
176 }
177
178 for iri in import_iris {
179 if let Some(local_path) = resolve_iri_to_path(&iri, &state.import_paths) {
180 if cfs::exists(fs, &local_path).await {
181 self.collect_nodes_from_file(fs, &local_path, state, all_nodes, visited)
182 .await?;
183 }
184 }
185 }
186
187 Ok(())
188 })
189 }
190
191 fn process_merged_nodes(
193 &mut self,
194 all_nodes: &HashMap<String, CollectedNode>,
195 _state: &ModuleState,
196 ) -> Result<()> {
197 for node in all_nodes.values() {
199 if node.types.contains(&IRI_MODULE.to_string()) {
200 self.register_module_from_merged(node, all_nodes)?;
201 }
202 }
203 Ok(())
204 }
205
206 fn register_module_from_merged(
207 &mut self,
208 node: &CollectedNode,
209 _all_nodes: &HashMap<String, CollectedNode>,
210 ) -> Result<()> {
211 let require_name = node
212 .properties
213 .get(IRI_DOAP_NAME)
214 .and_then(|v| v.first())
215 .and_then(|v| v.as_str())
216 .map(String::from);
217
218 let mut components = Vec::new();
219
220 if let Some(component_vals) = node.properties.get(IRI_COMPONENT) {
221 for comp_val in component_vals {
222 if let Some(comp) = self.parse_component(comp_val, &node.id) {
223 self.components.insert(comp.iri.clone(), comp.clone());
224 components.push(comp);
225 }
226 }
227 }
228
229 let module = CjsModule {
230 iri: node.id.clone(),
231 require_name,
232 components,
233 source_file: node.source_file.clone(),
234 };
235
236 self.modules.insert(node.id.clone(), module);
237 Ok(())
238 }
239
240 fn parse_component(
241 &self,
242 value: &serde_json::Value,
243 module_iri: &str,
244 ) -> Option<CjsComponent> {
245 let obj = value.as_object()?;
246
247 let iri = obj.get("@id").and_then(|v| v.as_str())?.to_string();
248
249 let types: Vec<String> = match obj.get("@type") {
250 Some(serde_json::Value::String(t)) => vec![t.clone()],
251 Some(serde_json::Value::Array(arr)) => {
252 arr.iter()
253 .filter_map(|v| v.as_str().map(String::from))
254 .collect()
255 }
256 _ => vec![],
257 };
258
259 let component_type = ComponentType::from_type_iris(&types).or_else(|| {
261 for t in &types {
262 match t.as_str() {
263 "Class" => return Some(ComponentType::Class),
264 "AbstractClass" => return Some(ComponentType::AbstractClass),
265 "Instance" => return Some(ComponentType::Instance),
266 _ => {}
267 }
268 }
269 None
270 })?;
271
272 let require_element = obj
273 .get("requireElement")
274 .or_else(|| obj.get(IRI_COMPONENT_PATH))
275 .and_then(|v| v.as_str())
276 .map(String::from);
277
278 let comment = obj
279 .get("comment")
280 .or_else(|| obj.get(IRI_RDFS_COMMENT))
281 .and_then(|v| v.as_str())
282 .map(String::from);
283
284 let parameters = self.parse_parameters(obj);
285
286 let extends: Vec<String> = match obj
287 .get("extends")
288 .or_else(|| obj.get(IRI_RDFS_SUBCLASS_OF))
289 {
290 Some(serde_json::Value::String(s)) => vec![s.clone()],
291 Some(serde_json::Value::Array(arr)) => arr
292 .iter()
293 .filter_map(|v| match v {
294 serde_json::Value::String(s) => Some(s.clone()),
295 serde_json::Value::Object(o) => {
296 o.get("@id").and_then(|v| v.as_str()).map(String::from)
297 }
298 _ => None,
299 })
300 .collect(),
301 Some(serde_json::Value::Object(o)) => o
302 .get("@id")
303 .and_then(|v| v.as_str())
304 .map(String::from)
305 .into_iter()
306 .collect(),
307 _ => vec![],
308 };
309
310 let constructor_arguments = obj
311 .get("constructorArguments")
312 .or_else(|| obj.get(IRI_CONSTRUCTOR_ARGUMENTS))
313 .cloned();
314
315 Some(CjsComponent {
316 iri,
317 component_type,
318 require_element,
319 comment,
320 parameters,
321 extends,
322 constructor_arguments,
323 module_iri: Some(module_iri.to_string()),
324 })
325 }
326
327 fn parse_parameters(
328 &self,
329 obj: &serde_json::Map<String, serde_json::Value>,
330 ) -> Vec<CjsParameter> {
331 let params_val = obj.get("parameters").or_else(|| obj.get(IRI_PARAMETER));
332
333 let params_arr = match params_val {
334 Some(serde_json::Value::Array(arr)) => arr,
335 _ => return vec![],
336 };
337
338 params_arr
339 .iter()
340 .filter_map(|p| {
341 let p_obj = p.as_object()?;
342 let iri = p_obj.get("@id").and_then(|v| v.as_str())?.to_string();
343 let range = p_obj
344 .get("range")
345 .or_else(|| p_obj.get(IRI_RDFS_RANGE))
346 .and_then(|v| match v {
347 serde_json::Value::String(s) => Some(s.clone()),
348 serde_json::Value::Object(o) => {
349 o.get("@id").and_then(|v| v.as_str()).map(String::from)
350 }
351 _ => None,
352 });
353 let comment = p_obj
354 .get("comment")
355 .or_else(|| p_obj.get(IRI_RDFS_COMMENT))
356 .and_then(|v| v.as_str())
357 .map(String::from);
358 let required = p_obj
359 .get("required")
360 .and_then(|v| v.as_bool())
361 .unwrap_or(false);
362 let lazy = p_obj
363 .get("lazy")
364 .and_then(|v| v.as_bool())
365 .unwrap_or(false);
366 let unique = p_obj
367 .get("unique")
368 .and_then(|v| v.as_bool())
369 .unwrap_or(false);
370 let default_value = p_obj.get("default").cloned();
371
372 Some(CjsParameter {
373 iri,
374 range,
375 comment,
376 required,
377 lazy,
378 unique,
379 default_value,
380 })
381 })
382 .collect()
383 }
384
385 pub fn finalize(&mut self) {
387 let component_iris: Vec<String> = self.components.keys().cloned().collect();
388 for iri in component_iris {
389 let inherited_params = self.collect_inherited_params(&iri, &mut Vec::new());
390 if let Some(comp) = self.components.get_mut(&iri) {
391 for param in inherited_params {
392 if !comp.parameters.iter().any(|p| p.iri == param.iri) {
393 comp.parameters.push(param);
394 }
395 }
396 }
397 }
398 }
399
400 fn collect_inherited_params(
401 &self,
402 iri: &str,
403 visited: &mut Vec<String>,
404 ) -> Vec<CjsParameter> {
405 if visited.contains(&iri.to_string()) {
406 return vec![];
407 }
408 visited.push(iri.to_string());
409
410 let Some(comp) = self.components.get(iri) else {
411 return vec![];
412 };
413
414 let mut params = Vec::new();
415 for parent_iri in &comp.extends.clone() {
416 if let Some(parent) = self.components.get(parent_iri) {
417 params.extend(parent.parameters.clone());
418 }
419 params.extend(self.collect_inherited_params(parent_iri, visited));
420 }
421 params
422 }
423}
424
425fn collect_import_iris(value: &serde_json::Value, resolver: &ContextResolver, out: &mut Vec<String>) {
426 match value {
427 serde_json::Value::String(s) => out.push(resolver.expand_term(s)),
428 serde_json::Value::Array(arr) => {
429 for v in arr {
430 if let Some(s) = v.as_str() {
431 out.push(resolver.expand_term(s));
432 }
433 }
434 }
435 _ => {}
436 }
437}
438
439pub fn resolve_iri_to_path(
441 iri: &str,
442 import_paths: &HashMap<String, PathBuf>,
443) -> Option<PathBuf> {
444 for (prefix_iri, local_dir) in import_paths {
445 if iri.starts_with(prefix_iri.as_str()) {
446 let suffix = &iri[prefix_iri.len()..];
447 return Some(local_dir.join(suffix));
448 }
449 }
450 if let Some(path) = iri.strip_prefix("file://") {
451 return Some(PathBuf::from(path));
452 }
453 None
454}