use std::collections::HashMap;
use std::ops::Range;
use rdf_parsers::jsonld::convert::{parse_json, JsonLdVal};
use url::Url;
use crate::components::registry::{collect_id_spans, resolve_iri_to_url};
use crate::components::types::*;
use crate::context::expand::ContextResolver;
use crate::error::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,
url: &'a Url,
state: &'a ModuleState,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
tracing::debug!("Loading config file: {}", url.as_str());
let contents = fs.read_to_string(url).await?;
let Some(doc) = parse_json(&contents) else {
tracing::warn!("Failed to parse config file: {}", url.as_str());
return Ok(());
};
let resolver = if let Some(ctx) = doc.get("@context") {
ContextResolver::from_context_value(ctx, &state.contexts)?
} else {
ContextResolver::new()
};
let mut id_spans: HashMap<String, Range<usize>> = HashMap::new();
collect_id_spans(&doc, &resolver, &mut id_spans);
self.process_imports(fs, &doc, &resolver, state, url).await?;
let entries: Vec<&JsonLdVal> = if let Some(graph) = doc.get("@graph") {
match graph.as_array() {
Some(arr) => arr.iter().map(|(v, _)| v).collect(),
None => 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, url, &id_spans) {
self.configs.push(config);
}
}
Ok(())
})
}
async fn process_imports(
&mut self,
fs: &dyn Fs,
doc: &JsonLdVal,
resolver: &ContextResolver,
state: &ModuleState,
source_url: &Url,
) -> Result<()> {
if let Some(import_val) = doc.get("import") {
let iris: Vec<String> = match import_val {
JsonLdVal::Str(s) => vec![resolver.expand_term(s)],
_ => import_val
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|(v, _)| v.as_str())
.map(|s| resolver.expand_term(s))
.collect()
})
.unwrap_or_default(),
};
for iri in iris {
if let Some(local_url) = resolve_iri_to_url(&iri, &state.import_paths) {
if cfs::exists(fs, &local_url).await && &local_url != source_url {
self.load_config_file(fs, &local_url, state).await?;
}
}
}
}
Ok(())
}
fn parse_config_entry(
&self,
value: &JsonLdVal,
resolver: &ContextResolver,
source_url: &Url,
id_spans: &HashMap<String, Range<usize>>,
) -> Option<ConfigInstance> {
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 type_iri = match value.get("@type") {
Some(JsonLdVal::Str(t)) => resolver.expand_term(t),
Some(v) => v
.as_array()?
.iter()
.filter_map(|(item, _)| item.as_str())
.map(|s| resolver.expand_term(s))
.next()?,
None => return None,
};
if type_iri.contains("Override") {
return None;
}
let mut parameters = HashMap::new();
if let Some(members) = value.as_object() {
for (key, _, _, val) in members {
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_url.to_string(),
iri_span,
})
}
async fn load_config_directory(
&mut self,
fs: &dyn Fs,
dir: &Url,
state: &ModuleState,
) -> Result<()> {
if !fs.is_dir(dir).await {
return Ok(());
}
let files = cfs::walk_dir(fs, dir).await?;
for url in files {
let is_config = std::path::Path::new(url.path())
.extension()
.is_some_and(|ext| ext == "jsonld" || ext == "json");
if is_config {
self.load_config_file(fs, &url, state).await?;
}
}
Ok(())
}
}