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;
15
16pub struct LinkReferenceValidator {
17 trace: bool,
18 auto_create_missing_references: bool,
19}
20
21#[derive(Debug, Default)]
22struct LinkReferencePlan {
23 numeric_ids_to_be_created: HashSet<u32>,
24 names_to_be_created: HashSet<String>,
25 composite_pairs_to_be_created: HashSet<(u32, u32)>,
32 missing_references: Vec<MissingLinkReference>,
33 missing_reference_keys: HashSet<String>,
34}
35
36impl LinkReferencePlan {
37 fn add_missing_reference(&mut self, reference: MissingLinkReference) {
38 let key = reference.key();
39 if self.missing_reference_keys.insert(key) {
40 self.missing_references.push(reference);
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
46struct MissingLinkReference {
47 identifier: String,
48 pattern_type: &'static str,
49 numeric_id: Option<u32>,
50}
51
52impl MissingLinkReference {
53 fn key(&self) -> String {
54 self.numeric_id
55 .map(|id| format!("id:{id}"))
56 .unwrap_or_else(|| format!("name:{}", self.identifier))
57 }
58}
59
60impl LinkReferenceValidator {
61 pub fn new(trace: bool, auto_create_missing_references: bool) -> Self {
62 Self {
63 trace,
64 auto_create_missing_references,
65 }
66 }
67
68 pub fn validate_links_exist_or_will_be_created(
69 &self,
70 storage: &mut impl NamedTypeLinks,
71 restriction_patterns: &[LinoLink],
72 substitution_patterns: &[LinoLink],
73 ) -> Result<Vec<(Link, Link)>> {
74 self.trace_msg("[ValidateLinksExistOrWillBeCreated] Starting validation");
75
76 let mut plan = self.build_link_reference_plan(storage, substitution_patterns);
77 self.trace_msg(&format!(
78 "[ValidateLinksExistOrWillBeCreated] Numeric links to be created: {:?}",
79 plan.numeric_ids_to_be_created
80 ));
81 self.trace_msg(&format!(
82 "[ValidateLinksExistOrWillBeCreated] Named links to be created: {:?}",
83 plan.names_to_be_created
84 ));
85
86 self.collect_missing_references(
87 storage,
88 &mut plan,
89 restriction_patterns,
90 false,
91 "restriction",
92 )?;
93 self.collect_missing_references(
94 storage,
95 &mut plan,
96 substitution_patterns,
97 true,
98 "substitution",
99 )?;
100
101 if plan.missing_references.is_empty() {
102 self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed");
103 return Ok(vec![]);
104 }
105
106 if !self.auto_create_missing_references {
107 let missing = &plan.missing_references[0];
108 return Err(LinkError::QueryError(format!(
109 "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.",
110 missing.identifier, missing.pattern_type, missing.identifier
111 ))
112 .into());
113 }
114
115 let created = self.auto_create_missing_references(storage, &plan)?;
116 self.trace_msg("[ValidateLinksExistOrWillBeCreated] Validation completed");
117 Ok(created)
118 }
119
120 fn build_link_reference_plan(
121 &self,
122 storage: &mut impl NamedTypeLinks,
123 substitution_patterns: &[LinoLink],
124 ) -> LinkReferencePlan {
125 let mut plan = LinkReferencePlan::default();
126 let mut reserved_numeric_ids = HashSet::new();
127
128 for pattern in substitution_patterns {
129 self.collect_explicit_definitions(pattern, &mut plan, &mut reserved_numeric_ids);
130 }
131
132 for pattern in substitution_patterns {
133 self.collect_implicit_definitions(
134 storage,
135 pattern,
136 &mut plan,
137 &mut reserved_numeric_ids,
138 );
139 }
140
141 for pattern in substitution_patterns {
142 Self::collect_composite_pairs(pattern, &mut plan);
143 }
144
145 plan
146 }
147
148 fn collect_explicit_definitions(
149 &self,
150 pattern: &LinoLink,
151 plan: &mut LinkReferencePlan,
152 reserved_numeric_ids: &mut HashSet<u32>,
153 ) {
154 if Self::is_composite_lino(pattern) {
155 if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) {
156 if let Ok(link_id) = identifier.parse::<u32>() {
157 plan.numeric_ids_to_be_created.insert(link_id);
158 reserved_numeric_ids.insert(link_id);
159 } else {
160 plan.names_to_be_created.insert(identifier);
161 }
162 }
163 }
164
165 if let Some(values) = &pattern.values {
166 for sub_pattern in values {
167 self.collect_explicit_definitions(sub_pattern, plan, reserved_numeric_ids);
168 }
169 }
170 }
171
172 fn collect_composite_pairs(pattern: &LinoLink, plan: &mut LinkReferencePlan) {
173 if Self::is_composite_lino(pattern)
174 && Self::concrete_identifier(pattern.id.as_deref()).is_some()
175 {
176 if let Some(values) = &pattern.values {
177 if let (Some(source), Some(target)) = (
178 Self::concrete_numeric_identifier(values[0].id.as_deref()),
179 Self::concrete_numeric_identifier(values[1].id.as_deref()),
180 ) {
181 plan.composite_pairs_to_be_created.insert((source, target));
182 }
183 }
184 }
185
186 if let Some(values) = &pattern.values {
187 for sub_pattern in values {
188 Self::collect_composite_pairs(sub_pattern, plan);
189 }
190 }
191 }
192
193 fn collect_implicit_definitions(
194 &self,
195 storage: &mut impl NamedTypeLinks,
196 pattern: &LinoLink,
197 plan: &mut LinkReferencePlan,
198 reserved_numeric_ids: &mut HashSet<u32>,
199 ) {
200 if let Some(values) = &pattern.values {
201 for sub_pattern in values {
202 self.collect_implicit_definitions(storage, sub_pattern, plan, reserved_numeric_ids);
203 }
204 }
205
206 if Self::is_composite_lino(pattern)
207 && Self::concrete_identifier(pattern.id.as_deref()).is_none()
208 {
209 let next_id = Self::next_available_link_id(storage, reserved_numeric_ids);
210 reserved_numeric_ids.insert(next_id);
211 plan.numeric_ids_to_be_created.insert(next_id);
212 }
213 }
214
215 fn next_available_link_id(
216 storage: &mut impl NamedTypeLinks,
217 reserved_numeric_ids: &HashSet<u32>,
218 ) -> u32 {
219 let mut next_id = 1;
220 while storage.exists(next_id) || reserved_numeric_ids.contains(&next_id) {
221 next_id += 1;
222 }
223 next_id
224 }
225
226 fn collect_missing_references(
227 &self,
228 storage: &mut impl NamedTypeLinks,
229 plan: &mut LinkReferencePlan,
230 patterns: &[LinoLink],
231 is_substitution: bool,
232 pattern_type: &'static str,
233 ) -> Result<()> {
234 for pattern in patterns {
235 self.collect_missing_references_in_pattern(
236 storage,
237 plan,
238 pattern,
239 is_substitution,
240 pattern_type,
241 )?;
242 }
243 Ok(())
244 }
245
246 fn collect_missing_references_in_pattern(
247 &self,
248 storage: &mut impl NamedTypeLinks,
249 plan: &mut LinkReferencePlan,
250 pattern: &LinoLink,
251 is_substitution: bool,
252 pattern_type: &'static str,
253 ) -> Result<()> {
254 let pattern_id_is_definition = is_substitution
255 && Self::is_composite_lino(pattern)
256 && Self::concrete_identifier(pattern.id.as_deref()).is_some();
257
258 if !pattern_id_is_definition {
259 if let Some(identifier) = Self::concrete_identifier(pattern.id.as_deref()) {
260 self.validate_reference_identifier(storage, plan, &identifier, pattern_type)?;
261 }
262 }
263
264 if let Some(values) = &pattern.values {
265 for sub_pattern in values {
266 self.collect_missing_references_in_pattern(
267 storage,
268 plan,
269 sub_pattern,
270 is_substitution,
271 pattern_type,
272 )?;
273 }
274 }
275 Ok(())
276 }
277
278 fn validate_reference_identifier(
279 &self,
280 storage: &mut impl NamedTypeLinks,
281 plan: &mut LinkReferencePlan,
282 identifier: &str,
283 pattern_type: &'static str,
284 ) -> Result<()> {
285 if let Ok(link_id) = identifier.parse::<u32>() {
286 if !storage.exists(link_id) && !plan.numeric_ids_to_be_created.contains(&link_id) {
287 plan.add_missing_reference(MissingLinkReference {
288 identifier: identifier.to_string(),
289 pattern_type,
290 numeric_id: Some(link_id),
291 });
292 return Ok(());
293 }
294 self.trace_msg(&format!(
295 "[ValidateReferencesInPattern] Link {link_id} reference validated in {pattern_type} pattern"
296 ));
297 return Ok(());
298 }
299
300 if storage.get_by_name(identifier)?.is_none()
301 && !plan.names_to_be_created.contains(identifier)
302 {
303 plan.add_missing_reference(MissingLinkReference {
304 identifier: identifier.to_string(),
305 pattern_type,
306 numeric_id: None,
307 });
308 return Ok(());
309 }
310
311 self.trace_msg(&format!(
312 "[ValidateReferencesInPattern] Named link '{identifier}' reference validated in {pattern_type} pattern"
313 ));
314 Ok(())
315 }
316
317 fn auto_create_missing_references(
337 &self,
338 storage: &mut impl NamedTypeLinks,
339 plan: &LinkReferencePlan,
340 ) -> Result<Vec<(Link, Link)>> {
341 let missing_references = &plan.missing_references;
342 let mut created = Vec::new();
343 let mut numeric_references = missing_references
344 .iter()
345 .filter_map(|reference| reference.numeric_id)
346 .collect::<Vec<_>>();
347 numeric_references.sort_unstable();
348 numeric_references.dedup();
349
350 for link_id in numeric_references {
351 if storage.exists(link_id) {
352 continue;
353 }
354
355 self.trace_msg(&format!(
356 "[ValidateLinksExistOrWillBeCreated] Auto-creating missing numeric reference {link_id}."
357 ));
358 storage.try_ensure_created(link_id)?;
359 if plan
360 .composite_pairs_to_be_created
361 .contains(&(link_id, link_id))
362 {
363 self.trace_msg(&format!(
364 "[ValidateLinksExistOrWillBeCreated] Link {link_id} exists as a placeholder because ({link_id}, {link_id}) is defined by the substitution."
365 ));
366 continue;
367 }
368 let before = storage
369 .get_link(link_id)
370 .unwrap_or_else(|| Link::new(link_id, 0, 0));
371 storage.update(link_id, link_id, link_id)?;
372 if let Some(after) = storage.get_link(link_id) {
373 created.push((before, after));
374 }
375 }
376
377 let mut named_references = missing_references
378 .iter()
379 .filter(|reference| reference.numeric_id.is_none())
380 .map(|reference| reference.identifier.clone())
381 .collect::<Vec<_>>();
382 named_references.sort();
383 named_references.dedup();
384
385 for name in named_references {
386 if storage.get_by_name(&name)?.is_some() {
387 continue;
388 }
389
390 self.trace_msg(&format!(
391 "[ValidateLinksExistOrWillBeCreated] Auto-creating missing named reference '{name}' as point link."
392 ));
393 let link_id = storage.get_or_create_named(&name)?;
394 if let Some(after) = storage.get_link(link_id) {
395 created.push((Link::new(link_id, 0, 0), after));
396 }
397 }
398
399 Ok(created)
400 }
401
402 fn is_composite_lino(lino_link: &LinoLink) -> bool {
403 lino_link.values_count() == 2
404 }
405
406 fn concrete_numeric_identifier(id: Option<&str>) -> Option<u32> {
407 Self::concrete_identifier(id).and_then(|identifier| identifier.parse::<u32>().ok())
408 }
409
410 fn concrete_identifier(id: Option<&str>) -> Option<String> {
411 let identifier = id?.trim_end_matches(':');
412 if identifier.is_empty() || identifier == "*" || identifier.starts_with('$') {
413 None
414 } else {
415 Some(identifier.to_string())
416 }
417 }
418
419 fn trace_msg(&self, msg: &str) {
420 if self.trace {
421 eprintln!("{}", msg);
422 }
423 }
424}