use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::components::types::*;
use crate::context::expand::{self, ContextResolver, ExpandedNode};
use crate::error::{ComponentsJsError, Result};
use crate::fs::{self as cfs, Fs};
use crate::module_state::ModuleState;
#[derive(Debug, Clone)]
pub struct ComponentRegistry {
pub components: HashMap<String, CjsComponent>,
pub modules: HashMap<String, CjsModule>,
}
#[derive(Debug, Clone)]
struct CollectedNode {
id: String,
types: Vec<String>,
properties: HashMap<String, Vec<serde_json::Value>>,
source_file: String,
}
impl ComponentRegistry {
pub fn new() -> Self {
Self {
components: HashMap::new(),
modules: HashMap::new(),
}
}
pub async fn register_available_modules(
&mut self,
fs: &dyn Fs,
state: &ModuleState,
) -> Result<()> {
let mut all_nodes: HashMap<String, CollectedNode> = HashMap::new();
let mut visited_files: std::collections::HashSet<PathBuf> =
std::collections::HashSet::new();
for version_map in state.component_modules.values() {
for component_path in version_map.values() {
if cfs::exists(fs, component_path).await {
self.collect_nodes_from_file(
fs,
component_path,
state,
&mut all_nodes,
&mut visited_files,
)
.await?;
} else {
tracing::warn!(
"Component file does not exist: {}",
component_path.display()
);
}
}
}
tracing::info!(
"Collected {} unique nodes from component files",
all_nodes.len()
);
self.process_merged_nodes(&all_nodes, state)?;
Ok(())
}
fn collect_nodes_from_file<'a>(
&'a self,
fs: &'a dyn Fs,
path: &'a Path,
state: &'a ModuleState,
all_nodes: &'a mut HashMap<String, CollectedNode>,
visited: &'a mut std::collections::HashSet<PathBuf>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
let canonical = fs
.canonicalize(path)
.await
.unwrap_or_else(|_| path.to_path_buf());
if visited.contains(&canonical) {
return Ok(());
}
visited.insert(canonical.clone());
tracing::debug!("Loading component file: {}", path.display());
let contents = fs.read_to_string(path).await?;
let doc: serde_json::Value =
serde_json::from_str(&contents).map_err(|e| ComponentsJsError::JsonParse {
path: path.display().to_string(),
source: e,
})?;
let resolver = if let Some(ctx) = doc.get("@context") {
ContextResolver::from_context_value(ctx, &state.contexts)?
} else {
ContextResolver::new()
};
let nodes = expand::extract_graph_nodes(&doc, &state.contexts)?;
let source = path.display().to_string();
for node in &nodes {
if let Some(id) = &node.id {
let entry =
all_nodes
.entry(id.clone())
.or_insert_with(|| CollectedNode {
id: id.clone(),
types: Vec::new(),
properties: HashMap::new(),
source_file: source.clone(),
});
for t in &node.types {
if !entry.types.contains(t) {
entry.types.push(t.clone());
}
}
for (key, vals) in &node.properties {
entry
.properties
.entry(key.clone())
.or_default()
.extend(vals.clone());
}
}
}
self.process_imports_collect(fs, &doc, &nodes, &resolver, state, all_nodes, visited)
.await?;
Ok(())
})
}
fn process_imports_collect<'a>(
&'a self,
fs: &'a dyn Fs,
doc: &'a serde_json::Value,
nodes: &'a [ExpandedNode],
resolver: &'a ContextResolver,
state: &'a ModuleState,
all_nodes: &'a mut HashMap<String, CollectedNode>,
visited: &'a mut std::collections::HashSet<PathBuf>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
let mut import_iris = Vec::new();
if let Some(import_val) = doc.get("import") {
collect_import_iris(import_val, resolver, &mut import_iris);
}
for node in nodes {
if let Some(imports) = node.properties.get(IRI_RDFS_SEE_ALSO) {
for import_val in imports {
collect_import_iris(import_val, resolver, &mut import_iris);
}
}
}
for iri in import_iris {
if let Some(local_path) = resolve_iri_to_path(&iri, &state.import_paths) {
if cfs::exists(fs, &local_path).await {
self.collect_nodes_from_file(fs, &local_path, state, all_nodes, visited)
.await?;
}
}
}
Ok(())
})
}
fn process_merged_nodes(
&mut self,
all_nodes: &HashMap<String, CollectedNode>,
_state: &ModuleState,
) -> Result<()> {
for node in all_nodes.values() {
if node.types.contains(&IRI_MODULE.to_string()) {
self.register_module_from_merged(node, all_nodes)?;
}
}
Ok(())
}
fn register_module_from_merged(
&mut self,
node: &CollectedNode,
_all_nodes: &HashMap<String, CollectedNode>,
) -> Result<()> {
let require_name = node
.properties
.get(IRI_DOAP_NAME)
.and_then(|v| v.first())
.and_then(|v| v.as_str())
.map(String::from);
let mut components = Vec::new();
if let Some(component_vals) = node.properties.get(IRI_COMPONENT) {
for comp_val in component_vals {
if let Some(comp) = self.parse_component(comp_val, &node.id) {
self.components.insert(comp.iri.clone(), comp.clone());
components.push(comp);
}
}
}
let module = CjsModule {
iri: node.id.clone(),
require_name,
components,
source_file: node.source_file.clone(),
};
self.modules.insert(node.id.clone(), module);
Ok(())
}
fn parse_component(
&self,
value: &serde_json::Value,
module_iri: &str,
) -> Option<CjsComponent> {
let obj = value.as_object()?;
let iri = obj.get("@id").and_then(|v| v.as_str())?.to_string();
let types: Vec<String> = match obj.get("@type") {
Some(serde_json::Value::String(t)) => vec![t.clone()],
Some(serde_json::Value::Array(arr)) => {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}
_ => vec![],
};
let component_type = ComponentType::from_type_iris(&types).or_else(|| {
for t in &types {
match t.as_str() {
"Class" => return Some(ComponentType::Class),
"AbstractClass" => return Some(ComponentType::AbstractClass),
"Instance" => return Some(ComponentType::Instance),
_ => {}
}
}
None
})?;
let require_element = obj
.get("requireElement")
.or_else(|| obj.get(IRI_COMPONENT_PATH))
.and_then(|v| v.as_str())
.map(String::from);
let comment = obj
.get("comment")
.or_else(|| obj.get(IRI_RDFS_COMMENT))
.and_then(|v| v.as_str())
.map(String::from);
let parameters = self.parse_parameters(obj);
let extends: Vec<String> = match obj
.get("extends")
.or_else(|| obj.get(IRI_RDFS_SUBCLASS_OF))
{
Some(serde_json::Value::String(s)) => vec![s.clone()],
Some(serde_json::Value::Array(arr)) => arr
.iter()
.filter_map(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Object(o) => {
o.get("@id").and_then(|v| v.as_str()).map(String::from)
}
_ => None,
})
.collect(),
Some(serde_json::Value::Object(o)) => o
.get("@id")
.and_then(|v| v.as_str())
.map(String::from)
.into_iter()
.collect(),
_ => vec![],
};
let constructor_arguments = obj
.get("constructorArguments")
.or_else(|| obj.get(IRI_CONSTRUCTOR_ARGUMENTS))
.cloned();
Some(CjsComponent {
iri,
component_type,
require_element,
comment,
parameters,
extends,
constructor_arguments,
module_iri: Some(module_iri.to_string()),
})
}
fn parse_parameters(
&self,
obj: &serde_json::Map<String, serde_json::Value>,
) -> Vec<CjsParameter> {
let params_val = obj.get("parameters").or_else(|| obj.get(IRI_PARAMETER));
let params_arr = match params_val {
Some(serde_json::Value::Array(arr)) => arr,
_ => return vec![],
};
params_arr
.iter()
.filter_map(|p| {
let p_obj = p.as_object()?;
let iri = p_obj.get("@id").and_then(|v| v.as_str())?.to_string();
let range = p_obj
.get("range")
.or_else(|| p_obj.get(IRI_RDFS_RANGE))
.and_then(|v| match v {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Object(o) => {
o.get("@id").and_then(|v| v.as_str()).map(String::from)
}
_ => None,
});
let comment = p_obj
.get("comment")
.or_else(|| p_obj.get(IRI_RDFS_COMMENT))
.and_then(|v| v.as_str())
.map(String::from);
let required = p_obj
.get("required")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let lazy = p_obj
.get("lazy")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let unique = p_obj
.get("unique")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let default_value = p_obj.get("default").cloned();
Some(CjsParameter {
iri,
range,
comment,
required,
lazy,
unique,
default_value,
})
})
.collect()
}
pub fn finalize(&mut self) {
let component_iris: Vec<String> = self.components.keys().cloned().collect();
for iri in component_iris {
let inherited_params = self.collect_inherited_params(&iri, &mut Vec::new());
if let Some(comp) = self.components.get_mut(&iri) {
for param in inherited_params {
if !comp.parameters.iter().any(|p| p.iri == param.iri) {
comp.parameters.push(param);
}
}
}
}
}
fn collect_inherited_params(
&self,
iri: &str,
visited: &mut Vec<String>,
) -> Vec<CjsParameter> {
if visited.contains(&iri.to_string()) {
return vec![];
}
visited.push(iri.to_string());
let Some(comp) = self.components.get(iri) else {
return vec![];
};
let mut params = Vec::new();
for parent_iri in &comp.extends.clone() {
if let Some(parent) = self.components.get(parent_iri) {
params.extend(parent.parameters.clone());
}
params.extend(self.collect_inherited_params(parent_iri, visited));
}
params
}
}
fn collect_import_iris(value: &serde_json::Value, resolver: &ContextResolver, out: &mut Vec<String>) {
match value {
serde_json::Value::String(s) => out.push(resolver.expand_term(s)),
serde_json::Value::Array(arr) => {
for v in arr {
if let Some(s) = v.as_str() {
out.push(resolver.expand_term(s));
}
}
}
_ => {}
}
}
pub fn resolve_iri_to_path(
iri: &str,
import_paths: &HashMap<String, PathBuf>,
) -> Option<PathBuf> {
for (prefix_iri, local_dir) in import_paths {
if iri.starts_with(prefix_iri.as_str()) {
let suffix = &iri[prefix_iri.len()..];
return Some(local_dir.join(suffix));
}
}
if let Some(path) = iri.strip_prefix("file://") {
return Some(PathBuf::from(path));
}
None
}