components_rs/config/
registry.rs1use std::collections::HashMap;
12use std::ops::Range;
13
14use rdf_parsers::jsonld::convert::{parse_json, JsonLdVal};
15use url::Url;
16
17use crate::components::registry::{collect_id_spans, resolve_iri_to_url};
18use crate::components::types::*;
19use crate::context::expand::ContextResolver;
20use crate::error::Result;
21use crate::fs::{self as cfs, Fs};
22use crate::module_state::ModuleState;
23
24#[derive(Debug, Clone)]
33pub struct ConfigRegistry {
34 pub configs: Vec<ConfigInstance>,
35}
36
37impl ConfigRegistry {
38 pub fn new() -> Self {
39 Self {
40 configs: Vec::new(),
41 }
42 }
43
44 pub async fn discover_configs(&mut self, fs: &dyn Fs, state: &ModuleState) -> Result<()> {
46 for (iri_prefix, local_dir) in &state.import_paths {
47 if iri_prefix.contains("/config/") && fs.is_dir(local_dir).await {
48 self.load_config_directory(fs, local_dir, state).await?;
49 }
50 }
51 Ok(())
52 }
53
54 pub fn load_config_file<'a>(
56 &'a mut self,
57 fs: &'a dyn Fs,
58 url: &'a Url,
59 state: &'a ModuleState,
60 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
61 Box::pin(async move {
62 tracing::debug!("Loading config file: {}", url.as_str());
63
64 let contents = fs.read_to_string(url).await?;
65 let Some(doc) = parse_json(&contents) else {
66 tracing::warn!("Failed to parse config file: {}", url.as_str());
67 return Ok(());
68 };
69
70 let resolver = if let Some(ctx) = doc.get("@context") {
71 ContextResolver::from_context_value(ctx, &state.contexts)?
72 } else {
73 ContextResolver::new()
74 };
75
76 let mut id_spans: HashMap<String, Range<usize>> = HashMap::new();
77 collect_id_spans(&doc, &resolver, &mut id_spans);
78
79 self.process_imports(fs, &doc, &resolver, state, url).await?;
80
81 let entries: Vec<&JsonLdVal> = if let Some(graph) = doc.get("@graph") {
82 match graph.as_array() {
83 Some(arr) => arr.iter().map(|(v, _)| v).collect(),
84 None => vec![graph],
85 }
86 } else if doc.get("@id").is_some() {
87 vec![&doc]
88 } else {
89 vec![]
90 };
91
92 for entry in entries {
93 if let Some(config) = self.parse_config_entry(entry, &resolver, url, &id_spans) {
94 self.configs.push(config);
95 }
96 }
97
98 Ok(())
99 })
100 }
101
102 async fn process_imports(
103 &mut self,
104 fs: &dyn Fs,
105 doc: &JsonLdVal,
106 resolver: &ContextResolver,
107 state: &ModuleState,
108 source_url: &Url,
109 ) -> Result<()> {
110 if let Some(import_val) = doc.get("import") {
111 let iris: Vec<String> = match import_val {
112 JsonLdVal::Str(s) => vec![resolver.expand_term(s)],
113 _ => import_val
114 .as_array()
115 .map(|arr| {
116 arr.iter()
117 .filter_map(|(v, _)| v.as_str())
118 .map(|s| resolver.expand_term(s))
119 .collect()
120 })
121 .unwrap_or_default(),
122 };
123 for iri in iris {
124 if let Some(local_url) = resolve_iri_to_url(&iri, &state.import_paths) {
125 if cfs::exists(fs, &local_url).await && &local_url != source_url {
126 self.load_config_file(fs, &local_url, state).await?;
127 }
128 }
129 }
130 }
131 Ok(())
132 }
133
134 fn parse_config_entry(
135 &self,
136 value: &JsonLdVal,
137 resolver: &ContextResolver,
138 source_url: &Url,
139 id_spans: &HashMap<String, Range<usize>>,
140 ) -> Option<ConfigInstance> {
141 let id_str = value.get("@id")?.as_str()?;
142 let iri = resolver.expand_term(id_str);
143 let iri_span = id_spans.get(&iri).cloned().unwrap_or(0..0);
144
145 let type_iri = match value.get("@type") {
146 Some(JsonLdVal::Str(t)) => resolver.expand_term(t),
147 Some(v) => v
148 .as_array()?
149 .iter()
150 .filter_map(|(item, _)| item.as_str())
151 .map(|s| resolver.expand_term(s))
152 .next()?,
153 None => return None,
154 };
155
156 if type_iri.contains("Override") {
157 return None;
158 }
159
160 let mut parameters = HashMap::new();
161 if let Some(members) = value.as_object() {
162 for (key, _, _, val) in members {
163 if key.starts_with('@') {
164 continue;
165 }
166 let expanded_key = resolver.expand_term(key);
167 parameters.insert(expanded_key, val.clone());
168 }
169 }
170
171 Some(ConfigInstance {
172 iri,
173 component_type_iri: type_iri,
174 parameters,
175 source_file: source_url.to_string(),
176 iri_span,
177 })
178 }
179
180 async fn load_config_directory(
181 &mut self,
182 fs: &dyn Fs,
183 dir: &Url,
184 state: &ModuleState,
185 ) -> Result<()> {
186 if !fs.is_dir(dir).await {
187 return Ok(());
188 }
189
190 let files = cfs::walk_dir(fs, dir).await?;
191 for url in files {
192 let is_config = std::path::Path::new(url.path())
193 .extension()
194 .is_some_and(|ext| ext == "jsonld" || ext == "json");
195 if is_config {
196 self.load_config_file(fs, &url, state).await?;
197 }
198 }
199
200 Ok(())
201 }
202}