use std::collections::HashMap;
use std::ops::Range;
use rdf_parsers::jsonld::convert::{parse_json, JsonLdVal};
use url::Url;
use crate::components::types::*;
use crate::context::expand::{self, ContextResolver, ExpandedNode};
use crate::error::Result;
use crate::fs::{self as cfs, Fs};
use crate::module_state::ModuleState;
pub fn collect_id_spans(
val: &JsonLdVal,
resolver: &ContextResolver,
out: &mut HashMap<String, Range<usize>>,
) {
match val {
JsonLdVal::Object(members, _) => {
for (key, _key_span, val_span, value) in members {
if key == "@id" {
if let Some(s) = value.as_str() {
let expanded = resolver.expand_term(s);
out.entry(expanded).or_insert_with(|| val_span.clone());
}
}
collect_id_spans(value, resolver, out);
}
}
JsonLdVal::Array(items) => {
for (item, _) in items {
collect_id_spans(item, resolver, out);
}
}
_ => {}
}
}
pub fn collect_id_sources(
val: &JsonLdVal,
resolver: &ContextResolver,
source_file: &str,
out: &mut HashMap<String, String>,
) {
match val {
JsonLdVal::Object(members, _) => {
for (key, _, _, value) in members {
if key == "@id" {
if let Some(s) = value.as_str() {
let expanded = resolver.expand_term(s);
out.entry(expanded)
.or_insert_with(|| source_file.to_string());
}
}
collect_id_sources(value, resolver, source_file, out);
}
}
JsonLdVal::Array(items) => {
for (item, _) in items {
collect_id_sources(item, resolver, source_file, out);
}
}
_ => {}
}
}
#[derive(Debug, Clone)]
pub struct ComponentRegistry {
pub components: HashMap<String, CjsComponent>,
pub modules: HashMap<String, CjsModule>,
pub parameters: HashMap<String, (String, Range<usize>)>,
pub file_sources: HashMap<String, String>,
}
#[derive(Debug, Clone)]
struct CollectedNode {
id: String,
types: Vec<String>,
properties: HashMap<String, Vec<JsonLdVal>>,
source_file: String,
id_span: Range<usize>,
resolver: ContextResolver,
}
impl ComponentRegistry {
pub fn new() -> Self {
Self {
components: HashMap::new(),
modules: HashMap::new(),
parameters: HashMap::new(),
file_sources: 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<Url> =
std::collections::HashSet::new();
let mut id_spans: HashMap<String, Range<usize>> = HashMap::new();
let mut id_source_files: HashMap<String, String> = HashMap::new();
let mut file_sources: HashMap<String, String> = HashMap::new();
let mut resolver_cache: HashMap<String, ContextResolver> = HashMap::new();
for version_map in state.component_modules.values() {
for component_url in version_map.values() {
if cfs::exists(fs, component_url).await {
self.collect_nodes_from_file(
fs,
component_url,
state,
&mut all_nodes,
&mut visited_files,
&mut id_spans,
&mut id_source_files,
&mut file_sources,
&mut resolver_cache,
)
.await?;
} else {
tracing::warn!(
"Component file does not exist: {}",
component_url.as_str()
);
}
}
}
tracing::info!(
"Collected {} unique nodes from component files",
all_nodes.len()
);
self.process_merged_nodes(&all_nodes, &id_spans, &id_source_files, state)?;
self.file_sources = file_sources;
Ok(())
}
fn collect_nodes_from_file<'a>(
&'a self,
fs: &'a dyn Fs,
url: &'a Url,
state: &'a ModuleState,
all_nodes: &'a mut HashMap<String, CollectedNode>,
visited: &'a mut std::collections::HashSet<Url>,
id_spans: &'a mut HashMap<String, Range<usize>>,
id_source_files: &'a mut HashMap<String, String>,
file_sources: &'a mut HashMap<String, String>,
resolver_cache: &'a mut HashMap<String, ContextResolver>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a + Send>> {
Box::pin(async move {
if visited.contains(url) {
return Ok(());
}
visited.insert(url.clone());
tracing::debug!("Loading component file: {}", url.as_str());
let contents = fs.read_to_string(url).await?;
let Some(doc) = parse_json(&contents) else {
tracing::warn!("Failed to parse component file: {}", url.as_str());
return Ok(());
};
let resolver = if let Some(ctx) = doc.get("@context") {
let key = context_cache_key(ctx);
if let Some(cached) = resolver_cache.get(&key) {
cached.clone()
} else {
let r = ContextResolver::from_context_value(ctx, &state.contexts)?;
resolver_cache.insert(key, r.clone());
r
}
} else {
ContextResolver::new()
};
collect_id_spans(&doc, &resolver, id_spans);
let nodes = expand::extract_graph_nodes(&doc, &state.contexts)?;
let source = url.to_string();
collect_id_sources(&doc, &resolver, &source, id_source_files);
file_sources.insert(source.clone(), contents);
for node in &nodes {
if let Some(id) = &node.id {
let span = id_spans.get(id).cloned().unwrap_or(0..0);
let entry = all_nodes
.entry(id.clone())
.or_insert_with(|| CollectedNode {
id: id.clone(),
types: Vec::new(),
properties: HashMap::new(),
source_file: source.clone(),
id_span: span,
resolver: resolver.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,
id_spans,
id_source_files,
file_sources,
resolver_cache,
)
.await?;
Ok(())
})
}
fn process_imports_collect<'a>(
&'a self,
fs: &'a dyn Fs,
doc: &'a JsonLdVal,
nodes: &'a [ExpandedNode],
resolver: &'a ContextResolver,
state: &'a ModuleState,
all_nodes: &'a mut HashMap<String, CollectedNode>,
visited: &'a mut std::collections::HashSet<Url>,
id_spans: &'a mut HashMap<String, Range<usize>>,
id_source_files: &'a mut HashMap<String, String>,
file_sources: &'a mut HashMap<String, String>,
resolver_cache: &'a mut HashMap<String, ContextResolver>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a + Send>> {
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_url) = resolve_iri_to_url(&iri, &state.import_paths) {
if cfs::exists(fs, &local_url).await {
self.collect_nodes_from_file(
fs,
&local_url,
state,
all_nodes,
visited,
id_spans,
id_source_files,
file_sources,
resolver_cache,
)
.await?;
}
}
}
Ok(())
})
}
fn process_merged_nodes(
&mut self,
all_nodes: &HashMap<String, CollectedNode>,
id_spans: &HashMap<String, Range<usize>>,
id_source_files: &HashMap<String, String>,
_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, id_spans, id_source_files)?;
}
}
Ok(())
}
fn register_module_from_merged(
&mut self,
node: &CollectedNode,
_all_nodes: &HashMap<String, CollectedNode>,
id_spans: &HashMap<String, Range<usize>>,
id_source_files: &HashMap<String, String>,
) -> 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,
&node.resolver,
id_spans,
id_source_files,
&node.source_file,
) {
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(),
iri_span: node.id_span.clone(),
};
self.modules.insert(node.id.clone(), module);
Ok(())
}
fn parse_component(
&self,
value: &JsonLdVal,
module_iri: &str,
resolver: &ContextResolver,
id_spans: &HashMap<String, Range<usize>>,
id_source_files: &HashMap<String, String>,
fallback_source_file: &str,
) -> Option<CjsComponent> {
let id_str = value.get("@id")?.as_str()?;
let iri = resolver.expand_term(id_str);
let iri_span = id_spans.get(&iri).cloned().unwrap_or(0..0);
let source_file = id_source_files
.get(&iri)
.cloned()
.unwrap_or_else(|| fallback_source_file.to_string());
let types: Vec<String> = match value.get("@type") {
Some(JsonLdVal::Str(t)) => vec![resolver.expand_term(t)],
Some(v) => v
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|(item, _)| item.as_str())
.map(|s| resolver.expand_term(s))
.collect()
})
.unwrap_or_default(),
None => 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 = value
.get("requireElement")
.or_else(|| value.get(IRI_COMPONENT_PATH))
.and_then(|v| v.as_str())
.map(String::from);
let comment = value
.get("comment")
.or_else(|| value.get(IRI_RDFS_COMMENT))
.and_then(|v| v.as_str())
.map(String::from);
let parameters =
self.parse_parameters(value, resolver, id_spans, id_source_files, &source_file);
let extends: Vec<String> = match value
.get("extends")
.or_else(|| value.get(IRI_RDFS_SUBCLASS_OF))
{
Some(JsonLdVal::Str(s)) => vec![resolver.expand_term(s)],
Some(v) if v.as_array().is_some() => v
.as_array()
.unwrap()
.iter()
.filter_map(|(item, _)| match item {
JsonLdVal::Str(s) => Some(resolver.expand_term(s)),
_ => item.get("@id")?.as_str().map(|s| resolver.expand_term(s)),
})
.collect(),
Some(v) => v
.get("@id")
.and_then(|v| v.as_str())
.map(|s| resolver.expand_term(s))
.into_iter()
.collect(),
None => vec![],
};
let constructor_arguments = value
.get("constructorArguments")
.or_else(|| value.get(IRI_CONSTRUCTOR_ARGUMENTS))
.cloned();
Some(CjsComponent {
iri,
component_type,
require_element,
comment,
parameters,
extends,
constructor_arguments,
module_iri: Some(module_iri.to_string()),
source_file,
iri_span,
})
}
fn parse_parameters(
&self,
value: &JsonLdVal,
resolver: &ContextResolver,
id_spans: &HashMap<String, Range<usize>>,
id_source_files: &HashMap<String, String>,
fallback_source_file: &str,
) -> Vec<CjsParameter> {
let params = match value.get("parameters").or_else(|| value.get(IRI_PARAMETER)) {
Some(v) => v,
None => return vec![],
};
let arr = match params.as_array() {
Some(a) => a,
None => return vec![],
};
arr.iter()
.filter_map(|(p, _)| {
let id_str = p.get("@id")?.as_str()?;
let iri = resolver.expand_term(id_str);
let iri_span = id_spans.get(&iri).cloned().unwrap_or(0..0);
let range =
p.get("range")
.or_else(|| p.get(IRI_RDFS_RANGE))
.and_then(|v| match v {
JsonLdVal::Str(s) => Some(resolver.expand_term(s)),
_ => v.get("@id")?.as_str().map(|s| resolver.expand_term(s)),
});
let comment = p
.get("comment")
.or_else(|| p.get(IRI_RDFS_COMMENT))
.and_then(|v| v.as_str())
.map(String::from);
let required = p.get("required").and_then(|v| v.as_bool()).unwrap_or(false);
let lazy = p.get("lazy").and_then(|v| v.as_bool()).unwrap_or(false);
let unique = p.get("unique").and_then(|v| v.as_bool()).unwrap_or(false);
let default_value = p.get("default").cloned();
let source_file = id_source_files
.get(&iri)
.cloned()
.unwrap_or_else(|| fallback_source_file.to_string());
Some(CjsParameter {
iri,
range,
comment,
required,
lazy,
unique,
default_value,
source_file,
iri_span,
})
})
.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 std::collections::HashSet::new());
if let Some(comp) = self.components.get_mut(&iri) {
let existing: std::collections::HashSet<String> =
comp.parameters.iter().map(|p| p.iri.clone()).collect();
for param in inherited_params {
if !existing.contains(¶m.iri) {
comp.parameters.push(param);
}
}
}
}
for comp in self.components.values() {
for param in &comp.parameters {
self.parameters
.entry(param.iri.clone())
.or_insert_with(|| (param.source_file.clone(), param.iri_span.clone()));
}
}
}
fn collect_inherited_params(
&self,
iri: &str,
visited: &mut std::collections::HashSet<String>,
) -> Vec<CjsParameter> {
if !visited.insert(iri.to_string()) {
return vec![];
}
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 context_cache_key(val: &JsonLdVal) -> String {
match val {
JsonLdVal::Str(s) => s.clone(),
JsonLdVal::Array(arr) => arr
.iter()
.map(|(v, _)| context_cache_key(v))
.collect::<Vec<_>>()
.join("\x00"),
_ => format!("{val:?}"),
}
}
fn collect_import_iris(value: &JsonLdVal, resolver: &ContextResolver, out: &mut Vec<String>) {
match value {
JsonLdVal::Str(s) => out.push(resolver.expand_term(s)),
_ => {
if let Some(arr) = value.as_array() {
for (item, _) in arr {
if let Some(s) = item.as_str() {
out.push(resolver.expand_term(s));
}
}
}
}
}
}
pub fn resolve_iri_to_url(
iri: &str,
import_paths: &std::collections::HashMap<String, Url>,
) -> Option<Url> {
for (prefix_iri, local_dir) in import_paths {
if iri.starts_with(prefix_iri.as_str()) {
let suffix = &iri[prefix_iri.len()..];
return local_dir.join(suffix).ok();
}
}
if iri.starts_with("file://") {
return Url::parse(iri).ok();
}
None
}