Skip to main content

link_cli/
link_reference_validator.rs

1//! Checking that every link a query refers to exists, and creating the ones
2//! that do not when the caller asked for that.
3//!
4//! Ported from the C# `LinkReferenceValidator`, and public for the same reason
5//! the query processor is: a custom front end that resolves references its own
6//! way needs to be able to reuse, or replace, exactly this step.
7
8use 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    /// `(source, target)` pairs the substitution itself defines.
26    ///
27    /// A missing numeric reference whose own point pair `(id, id)` appears
28    /// here is left as a `(id: 0 0)` placeholder instead of being turned into
29    /// a point link, so that the substitution which is about to write that
30    /// exact pair does not collide with it under uniqueness resolution.
31    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    /// Creates every missing reference and reports the `(before, after)` state
318    /// of each one.
319    ///
320    /// The before state is the placeholder the reference is turned into a point
321    /// link *from*, never `null`: both branches of the C# original create the
322    /// link silently — `EnsureCreated`, and `CreateAndUpdate(Null, Null)` with
323    /// no handler — and only pass the changes handler to the `Update` that
324    /// makes it a point link:
325    ///
326    /// ```csharp
327    /// links.Update(
328    ///   new DoubletLink(linkId, links.Constants.Null, links.Constants.Null),
329    ///   new DoubletLink(linkId, linkId, linkId),
330    ///   (beforeState, afterState) =>
331    ///       options.ChangesHandler?.Invoke(beforeState, afterState) ?? links.Constants.Continue
332    /// );
333    /// ```
334    ///
335    /// So `--changes` shows `((2: 0 0)) ((2: 2 2))`, not `() ((2: 2 2))`.
336    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}