Skip to main content

meta_language/
substitution.rs

1use std::collections::BTreeMap;
2
3use crate::link_network::{Link, LinkId};
4
5/// Match-and-substitute rule over exact link references.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct SubstitutionRule {
8    pattern: Vec<LinkId>,
9    replacement: Vec<LinkId>,
10}
11
12impl SubstitutionRule {
13    /// Creates a rule that replaces links with `pattern` references by
14    /// `replacement` references.
15    #[must_use]
16    pub fn new<const P: usize, const R: usize>(
17        pattern: [LinkId; P],
18        replacement: [LinkId; R],
19    ) -> Self {
20        Self {
21            pattern: pattern.to_vec(),
22            replacement: replacement.to_vec(),
23        }
24    }
25
26    /// Creates a rule that inserts a new relation link.
27    #[must_use]
28    pub fn create<const R: usize>(replacement: [LinkId; R]) -> Self {
29        Self {
30            pattern: Vec::new(),
31            replacement: replacement.to_vec(),
32        }
33    }
34
35    /// Creates a rule that deletes links matching `pattern`.
36    #[must_use]
37    pub fn delete<const P: usize>(pattern: [LinkId; P]) -> Self {
38        Self {
39            pattern: pattern.to_vec(),
40            replacement: Vec::new(),
41        }
42    }
43
44    pub(crate) fn pattern(&self) -> &[LinkId] {
45        &self.pattern
46    }
47
48    pub(crate) fn replacement(&self) -> &[LinkId] {
49        &self.replacement
50    }
51}
52
53/// A link reference or named variable in a substitution pattern.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum SubstitutionValue {
56    /// Exact link reference.
57    Link(LinkId),
58    /// Variable reference such as `$source`.
59    Variable(String),
60}
61
62impl SubstitutionValue {
63    /// Creates an exact link reference.
64    #[must_use]
65    pub const fn link(link_id: LinkId) -> Self {
66        Self::Link(link_id)
67    }
68
69    /// Creates a variable reference. A leading `$` is accepted and stripped.
70    #[must_use]
71    pub fn variable(name: impl Into<String>) -> Self {
72        Self::Variable(normalize_variable_name(name))
73    }
74}
75
76/// Match-and-substitute rule with link-cli-style variable bindings.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct VariableSubstitutionRule {
79    index_variable: Option<String>,
80    pattern: Vec<SubstitutionValue>,
81    replacement: Vec<SubstitutionValue>,
82}
83
84impl VariableSubstitutionRule {
85    /// Creates a variable substitution rule.
86    #[must_use]
87    pub fn new<const P: usize, const R: usize>(
88        pattern: [SubstitutionValue; P],
89        replacement: [SubstitutionValue; R],
90    ) -> Self {
91        Self {
92            index_variable: None,
93            pattern: Vec::from(pattern),
94            replacement: Vec::from(replacement),
95        }
96    }
97
98    /// Binds the matched link id to a variable such as `$index`.
99    #[must_use]
100    pub fn with_index_variable(mut self, name: impl Into<String>) -> Self {
101        self.index_variable = Some(normalize_variable_name(name));
102        self
103    }
104
105    pub(crate) fn index_variable(&self) -> Option<&str> {
106        self.index_variable.as_deref()
107    }
108
109    pub(crate) fn pattern(&self) -> &[SubstitutionValue] {
110        &self.pattern
111    }
112
113    pub(crate) fn replacement(&self) -> &[SubstitutionValue] {
114        &self.replacement
115    }
116
117    pub(crate) fn match_link(&self, link: &Link) -> Option<SubstitutionBindings> {
118        if link.references().len() != self.pattern.len() {
119            return None;
120        }
121
122        let mut bindings = SubstitutionBindings::default();
123        if let Some(index_variable) = self.index_variable() {
124            bindings.bind(index_variable, link.id())?;
125        }
126
127        for (pattern, reference) in self.pattern.iter().zip(link.references()) {
128            match pattern {
129                SubstitutionValue::Link(expected) if expected == reference => {}
130                SubstitutionValue::Link(_) => return None,
131                SubstitutionValue::Variable(name) => {
132                    bindings.bind(name, *reference)?;
133                }
134            }
135        }
136
137        Some(bindings)
138    }
139}
140
141/// Variables bound by one substitution match.
142#[derive(Clone, Debug, Default, PartialEq, Eq)]
143pub struct SubstitutionBindings {
144    values: BTreeMap<String, LinkId>,
145}
146
147impl SubstitutionBindings {
148    pub(crate) fn bind(&mut self, name: &str, link_id: LinkId) -> Option<()> {
149        match self.values.get(name) {
150            Some(existing) if *existing != link_id => None,
151            Some(_) => Some(()),
152            None => {
153                self.values.insert(normalize_variable_name(name), link_id);
154                Some(())
155            }
156        }
157    }
158
159    /// Returns the link bound to a variable name.
160    #[must_use]
161    pub fn get(&self, name: &str) -> Option<LinkId> {
162        self.values.get(&normalize_variable_name(name)).copied()
163    }
164
165    /// Iterates variable bindings in name order.
166    pub fn iter(&self) -> impl Iterator<Item = (&str, LinkId)> {
167        self.values
168            .iter()
169            .map(|(name, link_id)| (name.as_str(), *link_id))
170    }
171
172    pub(crate) fn resolve_values(&self, values: &[SubstitutionValue]) -> Option<Vec<LinkId>> {
173        values
174            .iter()
175            .map(|value| match value {
176                SubstitutionValue::Link(link_id) => Some(*link_id),
177                SubstitutionValue::Variable(name) => self.get(name),
178            })
179            .collect()
180    }
181}
182
183/// Result of applying a substitution rule.
184#[derive(Clone, Debug, Default, PartialEq, Eq)]
185pub struct SubstitutionReport {
186    pub(crate) created: Vec<LinkId>,
187    pub(crate) updated: Vec<LinkId>,
188    pub(crate) deleted: Vec<LinkId>,
189    pub(crate) bindings: Vec<SubstitutionBindings>,
190}
191
192impl SubstitutionReport {
193    /// Created link ids.
194    #[must_use]
195    pub fn created(&self) -> &[LinkId] {
196        &self.created
197    }
198
199    /// Updated link ids.
200    #[must_use]
201    pub fn updated(&self) -> &[LinkId] {
202        &self.updated
203    }
204
205    /// Deleted link ids.
206    #[must_use]
207    pub fn deleted(&self) -> &[LinkId] {
208        &self.deleted
209    }
210
211    /// Variable bindings for each matched substitution.
212    #[must_use]
213    pub fn bindings(&self) -> &[SubstitutionBindings] {
214        &self.bindings
215    }
216}
217
218fn normalize_variable_name(name: impl Into<String>) -> String {
219    name.into().trim_start_matches('$').to_string()
220}