Skip to main content

components_rs/components/
registry.rs

1use 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/// Registry of all discovered CJS components.
11#[derive(Debug, Clone)]
12pub struct ComponentRegistry {
13    /// All components indexed by IRI.
14    pub components: HashMap<String, CjsComponent>,
15    /// All modules indexed by IRI.
16    pub modules: HashMap<String, CjsModule>,
17}
18
19/// Intermediate collected node before merging.
20#[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    /// Register all available modules from the module state.
37    /// Uses a two-pass approach: collect all nodes from all files, merge by @id, then process.
38    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        // Phase 1: Load all component files and collect nodes
48        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        // Phase 2: Process merged nodes to find modules and components
74        self.process_merged_nodes(&all_nodes, state)?;
75
76        Ok(())
77    }
78
79    /// Recursively load a component file and its imports, collecting all nodes.
80    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            // Build context resolver
108            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            // Extract nodes
115            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            // Process imports
145            self.process_imports_collect(fs, &doc, &nodes, &resolver, state, all_nodes, visited)
146                .await?;
147
148            Ok(())
149        })
150    }
151
152    /// Process import references and recursively collect nodes from imported files.
153    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    /// Phase 2: Process merged nodes to extract modules and components.
192    fn process_merged_nodes(
193        &mut self,
194        all_nodes: &HashMap<String, CollectedNode>,
195        _state: &ModuleState,
196    ) -> Result<()> {
197        // Find all Module nodes
198        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        // Try expanded and short-name type matching
260        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    /// Finalize the registry: resolve inheritance (inherit parameters from extends chain).
386    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
439/// Resolve an IRI to a local file path using the import_paths mapping.
440pub 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}