1use anyhow::Result;
9use std::collections::HashSet;
10
11use crate::error::LinkError;
12use crate::link::Link;
13use crate::lino_link::LinoLink;
14use crate::named_type_links::NamedTypeLinks;
15use crate::query_processor::Changes;
16
17pub struct LinkReferenceValidator {
18 trace: bool,
19 auto_create_missing_references: bool,
20}
21
22#[derive(Debug, Default)]
23struct LinkReferencePlan {
24 numeric_ids_to_be_created: HashSet<u32>,
25 names_to_be_created: HashSet<String>,
26 composite_pairs_to_be_created: HashSet<(u32, u32)>,
33 missing_references: Vec<MissingLinkReference>,
34 missing_reference_keys: HashSet<String>,
35}
36
37impl LinkReferencePlan {
38 fn add_missing_reference(&mut self, reference: MissingLinkReference) {
39 let key = reference.key();
40 if self.missing_reference_keys.insert(key) {
41 self.missing_references.push(reference);
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
47struct MissingLinkReference {
48 identifier: String,
49 pattern_type: &'static str,
50 numeric_id: Option<u32>,
51}
52
53impl MissingLinkReference {
54 fn key(&self) -> String {
55 self.numeric_id
56 .map(|id| format!("id:{id}"))
57 .unwrap_or_else(|| format!("name:{}", self.identifier))
58 }
59}
60
61impl LinkReferenceValidator {
62 pub fn new(trace: bool, auto_create_missing_references: bool) -> Self {
63 Self {
64 trace,
65 auto_create_missing_references,
66 }
67 }
68
69 pub fn validate_links_exist_or_will_be_created(
70 &self,
71 storage: &mut impl NamedTypeLinks,
72 restriction_patterns: &[LinoLink],
73 substitution_patterns: &[LinoLink],
74 ) -> Result<Changes> {
75 self.trace_msg("[ValidateLinksExistOrWillBeCreated] Starting validation");
76
77 let mut created = Vec::new();
78 loop {
79 let plan =
80 self.plan_references(storage, restriction_patterns, substitution_patterns)?;
81 if plan.missing_references.is_empty() {
82 self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed");
83 return Ok(created);
84 }
85
86 if !self.auto_create_missing_references {
87 let missing = &plan.missing_references[0];
88 return Err(LinkError::QueryError(format!(
89 "Invalid reference to non-existent link '{}' in {} pattern. Link '{}' does not exist and will not be created by this operation. Use --auto-create-missing-references to create missing references as point links.",
90 missing.identifier, missing.pattern_type, missing.identifier
91 ))
92 .into());
93 }
94
95 created.extend(self.auto_create_missing_references(storage, &plan)?);
99 }
100 }
101
102 fn plan_references(
103 &self,
104 storage: &mut impl NamedTypeLinks,
105 restriction_patterns: &[LinoLink],
106 substitution_patterns: &[LinoLink],
107 ) -> Result<LinkReferencePlan> {
108 let mut plan = self.build_link_reference_plan(storage, substitution_patterns)?;
109 self.trace_msg(&format!(
110 "[ValidateLinksExistOrWillBeCreated] Numeric links to be created: {:?}",
111 plan.numeric_ids_to_be_created
112 ));
113 self.trace_msg(&format!(
114 "[ValidateLinksExistOrWillBeCreated] Named links to be created: {:?}",
115 plan.names_to_be_created
116 ));
117
118 self.collect_missing_references(
119 storage,
120 &mut plan,
121 restriction_patterns,
122 false,
123 "restriction",
124 )?;
125 self.collect_missing_references(
126 storage,
127 &mut plan,
128 substitution_patterns,
129 true,
130 "substitution",
131 )?;
132 Ok(plan)
133 }
134
135 fn build_link_reference_plan(
136 &self,
137 storage: &mut impl NamedTypeLinks,
138 substitution_patterns: &[LinoLink],
139 ) -> Result<LinkReferencePlan> {
140 let mut plan = LinkReferencePlan::default();
141 let mut anonymous_links = 0;
142 for pattern in substitution_patterns {
143 Self::collect_definitions(pattern, &mut plan, &mut anonymous_links);
144 }
145 plan.numeric_ids_to_be_created
146 .extend(Self::next_created_addresses(storage, anonymous_links)?);
147 Ok(plan)
148 }
149
150 fn collect_definitions(
154 pattern: &LinoLink,
155 plan: &mut LinkReferencePlan,
156 anonymous_links: &mut usize,
157 ) {
158 if let Some(values) = &pattern.values {
159 for sub_pattern in values {
160 Self::collect_definitions(sub_pattern, plan, anonymous_links);
161 }
162 }
163 if !Self::is_composite_lino(pattern) {
164 return;
165 }
166 let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) else {
167 *anonymous_links += 1;
168 return;
169 };
170 match identifier.parse::<u32>() {
171 Ok(link_id) => plan.numeric_ids_to_be_created.insert(link_id),
172 Err(_) => plan.names_to_be_created.insert(identifier),
173 };
174 let values = pattern.values.as_deref().unwrap_or_default();
175 if let (Some(source), Some(target)) = (
176 Self::concrete_numeric_identifier(values[0].id.as_deref()),
177 Self::concrete_numeric_identifier(values[1].id.as_deref()),
178 ) {
179 plan.composite_pairs_to_be_created.insert((source, target));
180 }
181 }
182
183 fn next_created_addresses(storage: &mut impl NamedTypeLinks, count: usize) -> Result<Vec<u32>> {
190 let addresses: Vec<u32> = (0..count).map(|_| storage.create(0, 0)).collect();
191 for &address in addresses.iter().rev() {
192 storage.delete(address)?;
193 }
194 Ok(addresses)
195 }
196
197 fn collect_missing_references(
198 &self,
199 storage: &mut impl NamedTypeLinks,
200 plan: &mut LinkReferencePlan,
201 patterns: &[LinoLink],
202 is_substitution: bool,
203 pattern_type: &'static str,
204 ) -> Result<()> {
205 for pattern in patterns {
206 self.collect_missing_references_in_pattern(
207 storage,
208 plan,
209 pattern,
210 is_substitution,
211 pattern_type,
212 )?;
213 }
214 Ok(())
215 }
216
217 fn collect_missing_references_in_pattern(
218 &self,
219 storage: &mut impl NamedTypeLinks,
220 plan: &mut LinkReferencePlan,
221 pattern: &LinoLink,
222 is_substitution: bool,
223 pattern_type: &'static str,
224 ) -> Result<()> {
225 let pattern_id_is_definition = is_substitution
226 && Self::is_composite_lino(pattern)
227 && Self::concrete_identifier(pattern.id.as_deref()).is_some();
228
229 if !pattern_id_is_definition {
230 if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) {
231 self.validate_reference_identifier(storage, plan, &identifier, pattern_type)?;
232 }
233 }
234
235 if let Some(values) = &pattern.values {
236 for sub_pattern in values {
237 self.collect_missing_references_in_pattern(
238 storage,
239 plan,
240 sub_pattern,
241 is_substitution,
242 pattern_type,
243 )?;
244 }
245 }
246 Ok(())
247 }
248
249 fn validate_reference_identifier(
250 &self,
251 storage: &mut impl NamedTypeLinks,
252 plan: &mut LinkReferencePlan,
253 identifier: &str,
254 pattern_type: &'static str,
255 ) -> Result<()> {
256 if let Ok(link_id) = identifier.parse::<u32>() {
257 if !storage.exists(link_id) && !plan.numeric_ids_to_be_created.contains(&link_id) {
258 plan.add_missing_reference(MissingLinkReference {
259 identifier: identifier.to_string(),
260 pattern_type,
261 numeric_id: Some(link_id),
262 });
263 return Ok(());
264 }
265 self.trace_msg(&format!(
266 "[ValidateReferencesInPattern] Link {link_id} reference validated in {pattern_type} pattern"
267 ));
268 return Ok(());
269 }
270
271 if storage.get_by_name(identifier)?.is_none()
272 && !plan.names_to_be_created.contains(identifier)
273 {
274 plan.add_missing_reference(MissingLinkReference {
275 identifier: identifier.to_string(),
276 pattern_type,
277 numeric_id: None,
278 });
279 return Ok(());
280 }
281
282 self.trace_msg(&format!(
283 "[ValidateReferencesInPattern] Named link '{identifier}' reference validated in {pattern_type} pattern"
284 ));
285 Ok(())
286 }
287
288 fn auto_create_missing_references(
297 &self,
298 storage: &mut impl NamedTypeLinks,
299 plan: &LinkReferencePlan,
300 ) -> Result<Changes> {
301 let missing_references = &plan.missing_references;
302 let mut created = Vec::new();
303 let mut numeric_references = missing_references
304 .iter()
305 .filter_map(|reference| reference.numeric_id)
306 .collect::<Vec<_>>();
307 numeric_references.sort_unstable();
308 numeric_references.dedup();
309
310 for link_id in numeric_references {
311 if storage.exists(link_id) {
312 continue;
313 }
314
315 self.trace_msg(&format!(
316 "[ValidateLinksExistOrWillBeCreated] Auto-creating missing numeric reference {link_id}."
317 ));
318 storage.try_ensure_created(link_id)?;
319 let placeholder = Link::new(link_id, 0, 0);
320 created.push((None, Some(placeholder)));
321 if plan
322 .composite_pairs_to_be_created
323 .contains(&(link_id, link_id))
324 {
325 self.trace_msg(&format!(
326 "[ValidateLinksExistOrWillBeCreated] Link {link_id} exists as a placeholder because ({link_id}, {link_id}) is defined by the substitution."
327 ));
328 continue;
329 }
330 storage.update(link_id, link_id, link_id)?;
331 created.push((Some(placeholder), storage.get_link(link_id)));
332 }
333
334 let mut named_references = missing_references
335 .iter()
336 .filter(|reference| reference.numeric_id.is_none())
337 .map(|reference| reference.identifier.clone())
338 .collect::<Vec<_>>();
339 named_references.sort();
340 named_references.dedup();
341
342 for name in named_references {
343 if storage.get_by_name(&name)?.is_some() {
344 continue;
345 }
346
347 self.trace_msg(&format!(
348 "[ValidateLinksExistOrWillBeCreated] Auto-creating missing named reference '{name}' as point link."
349 ));
350 let link_id = storage.get_or_create_named(&name)?;
351 created.push((None, storage.get_link(link_id)));
352 }
353
354 Ok(created)
355 }
356
357 fn is_composite_lino(lino_link: &LinoLink) -> bool {
358 lino_link.values_count() == 2
359 }
360
361 fn concrete_numeric_identifier(id: Option<&str>) -> Option<u32> {
362 Self::concrete_identifier(id).and_then(|identifier| identifier.parse::<u32>().ok())
363 }
364
365 fn concrete_identifier(id: Option<&str>) -> Option<String> {
366 let identifier = id?.trim_end_matches(':');
367 if identifier.is_empty() || identifier == "*" || identifier.starts_with('$') {
368 None
369 } else {
370 Some(identifier.to_string())
371 }
372 }
373
374 fn trace_msg(&self, msg: &str) {
375 if self.trace {
376 eprintln!("{}", msg);
377 }
378 }
379}