use std::collections::HashMap;
use std::path::Path;
use crate::components::types::*;
use crate::context::expand::ContextResolver;
use crate::error::{ComponentsJsError, Result};
use crate::fs::{self as cfs, Fs};
use crate::module_state::ModuleState;
#[derive(Debug, Clone)]
pub struct ConfigRegistry {
pub configs: Vec<ConfigInstance>,
}
impl ConfigRegistry {
pub fn new() -> Self {
Self {
configs: Vec::new(),
}
}
pub async fn discover_configs(
&mut self,
fs: &dyn Fs,
state: &ModuleState,
) -> Result<()> {
for (iri_prefix, local_dir) in &state.import_paths {
if iri_prefix.contains("/config/") && fs.is_dir(local_dir).await {
self.load_config_directory(fs, local_dir, state).await?;
}
}
Ok(())
}
pub fn load_config_file<'a>(
&'a mut self,
fs: &'a dyn Fs,
path: &'a Path,
state: &'a ModuleState,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + 'a>> {
Box::pin(async move {
tracing::debug!("Loading config 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()
};
self.process_imports(fs, &doc, &resolver, state, path).await?;
let entries: Vec<&serde_json::Value> = if let Some(graph) = doc.get("@graph") {
if let Some(arr) = graph.as_array() {
arr.iter().collect()
} else {
vec![graph]
}
} else if doc.get("@id").is_some() {
vec![&doc]
} else {
vec![]
};
for entry in entries {
if let Some(config) = self.parse_config_entry(entry, &resolver, path) {
self.configs.push(config);
}
}
Ok(())
})
}
async fn process_imports(
&mut self,
fs: &dyn Fs,
doc: &serde_json::Value,
resolver: &ContextResolver,
state: &ModuleState,
source_path: &Path,
) -> Result<()> {
if let Some(import_val) = doc.get("import") {
let iris = extract_import_iris(import_val, resolver);
for iri in iris {
if let Some(local_path) =
crate::components::registry::resolve_iri_to_path(&iri, &state.import_paths)
{
if cfs::exists(fs, &local_path).await && local_path != source_path {
self.load_config_file(fs, &local_path, state).await?;
}
}
}
}
Ok(())
}
fn parse_config_entry(
&self,
value: &serde_json::Value,
resolver: &ContextResolver,
source_path: &Path,
) -> Option<ConfigInstance> {
let obj = value.as_object()?;
let iri = obj
.get("@id")
.and_then(|v| v.as_str())
.map(|s| resolver.expand_term(s))?;
let type_iri = match obj.get("@type") {
Some(serde_json::Value::String(t)) => resolver.expand_term(t),
Some(serde_json::Value::Array(arr)) => {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| resolver.expand_term(s))
.next()?
}
_ => return None,
};
if type_iri.contains("Override") {
return None;
}
let mut parameters = HashMap::new();
for (key, val) in obj {
if key.starts_with('@') {
continue;
}
let expanded_key = resolver.expand_term(key);
parameters.insert(expanded_key, val.clone());
}
Some(ConfigInstance {
iri,
component_type_iri: type_iri,
parameters,
source_file: source_path.display().to_string(),
})
}
async fn load_config_directory(
&mut self,
fs: &dyn Fs,
dir: &Path,
state: &ModuleState,
) -> Result<()> {
if !fs.is_dir(dir).await {
return Ok(());
}
let files = cfs::walk_dir(fs, dir).await?;
for path in files {
let is_config = path
.extension()
.is_some_and(|ext| ext == "jsonld" || ext == "json");
if is_config {
self.load_config_file(fs, &path, state).await?;
}
}
Ok(())
}
}
fn extract_import_iris(value: &serde_json::Value, resolver: &ContextResolver) -> Vec<String> {
match value {
serde_json::Value::String(s) => vec![resolver.expand_term(s)],
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.as_str())
.map(|s| resolver.expand_term(s))
.collect(),
_ => vec![],
}
}