Skip to main content

link_cli/protocol/
links_operations.rs

1//! The links interface (`ILinks` in C#, [`doublets::Links`] in Rust) as LiNo
2//! documents, so a store behind a [`LinksServer`](super::LinksServer) is used
3//! exactly like a local one through [`RemoteLinks`](super::RemoteLinks).
4//!
5//! Every request is one top-level link named after the operation and holding
6//! exactly one value. The substitution query language ignores that shape (a
7//! query needs a restriction *and* a substitution), so an operation never
8//! collides with a query sent to the same server.
9//!
10//! | Request                          | Reply                                  |
11//! |----------------------------------|----------------------------------------|
12//! | `(count: (index source target))` | `(count: N)`                           |
13//! | `(each: (index source target))`  | one `(index: source target)` per match |
14//! | `(create: (source target))`      | `() ((index: source target))`          |
15//! | `(update: (index source target))`| `((index: s t)) ((index: source target))` per changed link |
16//! | `(delete: index)`                | `((index: source target)) ()` per removed link |
17//! | `(get-name: link)`               | `(name: 'text')`, or nothing           |
18//! | `(set-name: (link 'text'))`      | `(link: N)`                            |
19//! | `(get-by-name: 'text')`          | `(link: N)`, or nothing                |
20//! | `(remove-name: link)`            | nothing                                |
21//!
22//! A restriction holds up to three parts, matched like the raw links
23//! interface matches a query: none matches every link, `(index)` one link,
24//! `(index value)` links of that index whose source or target is `value`,
25//! and `(index source target)` matches part by part. `*` matches anything.
26//! Every reference in a reply is a number; failures are `(error: 'message')`.
27//!
28//! The changes of a `create`, `update` or `delete` are the net change of every
29//! link it touched, the way `clink --changes` reports a query: a cascading
30//! delete is one `((index: source target)) ()` per removed link, without the
31//! intermediate steps by which the store behind the server got there, which
32//! differ between the C# and the Rust stores.
33
34use super::error::{ProtocolError, ProtocolResult};
35use super::format::{format_document, format_link};
36use super::mapping::LinoDocument;
37use crate::changes_simplifier::simplify_changes;
38use crate::link::Link;
39use crate::named_type_links::NamedTypeLinks;
40use links_notation::LiNo;
41
42/// The wire spelling of "any value" in a restriction.
43pub const ANY: &str = "*";
44
45/// One part of a restriction: `None` matches any value.
46pub type Part = Option<u32>;
47
48/// One call of the links interface.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum LinksOperation {
51    /// Number of links matching the restriction.
52    Count(Vec<Part>),
53    /// Every link matching the restriction, ordered by index.
54    Each(Vec<Part>),
55    /// Creates a link.
56    Create { source: u32, target: u32 },
57    /// Points an existing link at a new source and target; an update that
58    /// would duplicate an existing link merges into it instead.
59    Update {
60        index: u32,
61        source: u32,
62        target: u32,
63    },
64    /// Deletes a link, and every link that still refers to it.
65    Delete(u32),
66    /// The name of a link.
67    GetName(u32),
68    /// Names a link.
69    SetName(u32, String),
70    /// The link with a name.
71    GetByName(String),
72    /// Removes the name of a link.
73    RemoveName(u32),
74}
75
76/// A `(before, after)` change; a missing side is [`Link::null`].
77pub type Change = (Link, Link);
78
79impl LinksOperation {
80    /// The request document for this operation.
81    pub fn to_document(&self) -> LinoDocument {
82        let (name, argument) = match self {
83            Self::Count(restriction) => ("count", restriction_lino(restriction)),
84            Self::Each(restriction) => ("each", restriction_lino(restriction)),
85            Self::Create { source, target } => ("create", numbers_lino(&[*source, *target])),
86            Self::Update {
87                index,
88                source,
89                target,
90            } => ("update", numbers_lino(&[*index, *source, *target])),
91            Self::Delete(index) => ("delete", number(*index)),
92            Self::GetName(index) => ("get-name", number(*index)),
93            Self::SetName(index, name) => (
94                "set-name",
95                group(vec![number(*index), LiNo::Ref(name.clone())]),
96            ),
97            Self::GetByName(name) => ("get-by-name", LiNo::Ref(name.clone())),
98            Self::RemoveName(index) => ("remove-name", number(*index)),
99        };
100        vec![named(name, vec![argument])]
101    }
102
103    /// Recognises an operation request; `Ok(None)` for any other document,
104    /// such as a substitution query.
105    pub fn from_document(document: &[LiNo<String>]) -> ProtocolResult<Option<Self>> {
106        let [LiNo::Link {
107            id: Some(name),
108            values,
109        }] = document
110        else {
111            return Ok(None);
112        };
113        let [argument] = values.as_slice() else {
114            return Ok(None);
115        };
116        let operation = match name.as_str() {
117            "count" => Self::Count(parse_restriction(argument)?),
118            "each" => Self::Each(parse_restriction(argument)?),
119            "create" => {
120                let [source, target] = parse_numbers::<2>(argument)?;
121                Self::Create { source, target }
122            }
123            "update" => {
124                let [index, source, target] = parse_numbers::<3>(argument)?;
125                Self::Update {
126                    index,
127                    source,
128                    target,
129                }
130            }
131            "delete" => Self::Delete(parse_number(argument)?),
132            "get-name" => Self::GetName(parse_number(argument)?),
133            "set-name" => match parts(argument) {
134                [index, LiNo::Ref(name)] => Self::SetName(parse_number(index)?, name.clone()),
135                _ => return Err(malformed_argument("set-name", argument)),
136            },
137            "get-by-name" => match argument {
138                LiNo::Ref(name) => Self::GetByName(name.clone()),
139                _ => return Err(malformed_argument("get-by-name", argument)),
140            },
141            "remove-name" => Self::RemoveName(parse_number(argument)?),
142            _ => return Ok(None),
143        };
144        Ok(Some(operation))
145    }
146
147    /// Runs the operation against `storage` and returns the reply document.
148    pub fn execute<S: NamedTypeLinks + ?Sized>(
149        &self,
150        storage: &mut S,
151    ) -> anyhow::Result<LinoDocument> {
152        Ok(match self {
153            Self::Count(restriction) => {
154                let count = matching(storage, restriction).len();
155                vec![named("count", vec![LiNo::Ref(count.to_string())])]
156            }
157            Self::Each(restriction) => matching(storage, restriction)
158                .iter()
159                .map(link_lino)
160                .collect(),
161            Self::Create { source, target } => {
162                // `create` makes exactly `(index: source target)`.
163                let created = Link::new(storage.create(*source, *target), *source, *target);
164                storage.save()?;
165                changes_document(&[(Link::null(), created)])
166            }
167            Self::Update {
168                index,
169                source,
170                target,
171            } => {
172                let mut changes = Vec::new();
173                storage.update_observed(*index, *source, *target, &mut |before, after| {
174                    changes.push((before, after))
175                })?;
176                storage.save()?;
177                changes_document(&simplify_changes(changes))
178            }
179            Self::Delete(index) => {
180                let mut changes = Vec::new();
181                storage
182                    .delete_observed(*index, &mut |before, after| changes.push((before, after)))?;
183                storage.save()?;
184                changes_document(&simplify_changes(changes))
185            }
186            Self::GetName(index) => match storage.get_name(*index)? {
187                Some(name) => vec![named("name", vec![LiNo::Ref(name)])],
188                None => Vec::new(),
189            },
190            Self::SetName(index, name) => {
191                let link = storage.set_name(*index, name)?;
192                storage.save()?;
193                link_reply(Some(link))
194            }
195            Self::GetByName(name) => link_reply(storage.get_by_name(name)?),
196            Self::RemoveName(index) => {
197                storage.remove_name(*index)?;
198                storage.save()?;
199                Vec::new()
200            }
201        })
202    }
203}
204
205/// Whether `link` matches `restriction`, with the shapes of the raw links
206/// interface: see the [module documentation](self).
207pub fn matches(link: &Link, restriction: &[Part]) -> bool {
208    let is = |part: Part, value: u32| part.is_none_or(|part| part == value);
209    match *restriction {
210        [] => true,
211        [index] => is(index, link.index),
212        [index, value] => {
213            is(index, link.index) && (is(value, link.source) || is(value, link.target))
214        }
215        [index, source, target] => {
216            is(index, link.index) && is(source, link.source) && is(target, link.target)
217        }
218        _ => false,
219    }
220}
221
222fn matching<S: NamedTypeLinks + ?Sized>(storage: &mut S, restriction: &[Part]) -> Vec<Link> {
223    let mut links = match restriction.first() {
224        Some(Some(index)) => storage.get_link(*index).into_iter().collect(),
225        _ => storage.all_links(),
226    };
227    links.retain(|link| matches(link, restriction));
228    links.sort_by_key(|link| link.index);
229    links
230}
231
232/// The `count` of a `(count: N)` reply.
233pub fn parse_count(document: &[LiNo<String>]) -> ProtocolResult<u32> {
234    match document {
235        [LiNo::Link {
236            id: Some(id),
237            values,
238        }] if id == "count" => match values.as_slice() {
239            [count] => parse_number(count),
240            _ => Err(malformed_reply("count", document)),
241        },
242        _ => Err(malformed_reply("count", document)),
243    }
244}
245
246/// The links of an `each` reply.
247pub fn parse_links(document: &[LiNo<String>]) -> ProtocolResult<Vec<Link>> {
248    document.iter().map(parse_link).collect()
249}
250
251/// The `(before) (after)` pairs of a `create`, `update` or `delete` reply.
252pub fn parse_changes(document: &[LiNo<String>]) -> ProtocolResult<Vec<Change>> {
253    document
254        .iter()
255        .map(|change| match parts(change) {
256            [before, after] => Ok((parse_change_side(before)?, parse_change_side(after)?)),
257            _ => Err(malformed_reply("change", document)),
258        })
259        .collect()
260}
261
262/// The name of a `get-name` reply.
263pub fn parse_name(document: &[LiNo<String>]) -> ProtocolResult<Option<String>> {
264    match document {
265        [] => Ok(None),
266        [LiNo::Link {
267            id: Some(id),
268            values,
269        }] if id == "name" => match values.as_slice() {
270            [LiNo::Ref(name)] => Ok(Some(name.clone())),
271            _ => Err(malformed_reply("name", document)),
272        },
273        _ => Err(malformed_reply("name", document)),
274    }
275}
276
277/// The link of a `set-name` or `get-by-name` reply.
278pub fn parse_link_reply(document: &[LiNo<String>]) -> ProtocolResult<Option<u32>> {
279    match document {
280        [] => Ok(None),
281        [LiNo::Link {
282            id: Some(id),
283            values,
284        }] if id == "link" => match values.as_slice() {
285            [link] => parse_number(link).map(Some),
286            _ => Err(malformed_reply("link", document)),
287        },
288        _ => Err(malformed_reply("link", document)),
289    }
290}
291
292/// A reply listing `changes`, one `(before) (after)` line each.
293pub fn changes_document(changes: &[Change]) -> LinoDocument {
294    changes
295        .iter()
296        .map(|(before, after)| group(vec![change_side(before), change_side(after)]))
297        .collect()
298}
299
300/// `(index: source target)` with plain numbers.
301pub fn link_lino(link: &Link) -> LiNo<String> {
302    named(
303        &link.index.to_string(),
304        vec![number(link.source), number(link.target)],
305    )
306}
307
308fn link_reply(link: Option<u32>) -> LinoDocument {
309    link.map(|link| vec![named("link", vec![number(link)])])
310        .unwrap_or_default()
311}
312
313fn change_side(link: &Link) -> LiNo<String> {
314    if link.is_null() {
315        group(Vec::new())
316    } else {
317        group(vec![link_lino(link)])
318    }
319}
320
321fn parse_change_side(side: &LiNo<String>) -> ProtocolResult<Link> {
322    match side {
323        LiNo::Link { id: None, values } if values.is_empty() => Ok(Link::null()),
324        LiNo::Link { id: None, values } if values.len() == 1 => parse_link(&values[0]),
325        // `((index: source target))` loses its wrapper when the side is the
326        // single named link itself.
327        link @ LiNo::Link { id: Some(_), .. } => parse_link(link),
328        _ => Err(ProtocolError::malformed(format!(
329            "expected a change side, found {}",
330            format_link(side)
331        ))),
332    }
333}
334
335fn parse_link(link: &LiNo<String>) -> ProtocolResult<Link> {
336    match link {
337        LiNo::Link {
338            id: Some(index),
339            values,
340        } => match values.as_slice() {
341            [source, target] => Ok(Link::new(
342                parse_text_number(index)?,
343                parse_number(source)?,
344                parse_number(target)?,
345            )),
346            _ => Err(ProtocolError::malformed(format!(
347                "expected (index: source target), found {}",
348                format_link(link)
349            ))),
350        },
351        _ => Err(ProtocolError::malformed(format!(
352            "expected (index: source target), found {}",
353            format_link(link)
354        ))),
355    }
356}
357
358fn restriction_lino(restriction: &[Part]) -> LiNo<String> {
359    group(
360        restriction
361            .iter()
362            .map(|part| part.map_or_else(|| LiNo::Ref(ANY.to_string()), number))
363            .collect(),
364    )
365}
366
367fn parse_restriction(argument: &LiNo<String>) -> ProtocolResult<Vec<Part>> {
368    let parts = parts(argument);
369    if parts.len() > 3 {
370        return Err(malformed_argument("restriction", argument));
371    }
372    parts
373        .iter()
374        .map(|part| match part {
375            LiNo::Ref(text) if text == ANY => Ok(None),
376            part => parse_number(part).map(Some),
377        })
378        .collect()
379}
380
381fn numbers_lino(numbers: &[u32]) -> LiNo<String> {
382    group(numbers.iter().copied().map(number).collect())
383}
384
385fn parse_numbers<const N: usize>(argument: &LiNo<String>) -> ProtocolResult<[u32; N]> {
386    let numbers = parts(argument)
387        .iter()
388        .map(parse_number)
389        .collect::<ProtocolResult<Vec<_>>>()?;
390    numbers
391        .try_into()
392        .map_err(|_| malformed_argument(&format!("{N} numbers"), argument))
393}
394
395/// The values of an unnamed group; a lone reference is a group of one, since
396/// the canonical document model unwraps `(x)` to `x`.
397fn parts(argument: &LiNo<String>) -> &[LiNo<String>] {
398    match argument {
399        LiNo::Link { id: None, values } => values,
400        reference => std::slice::from_ref(reference),
401    }
402}
403
404fn parse_number(value: &LiNo<String>) -> ProtocolResult<u32> {
405    match value {
406        LiNo::Ref(text) => parse_text_number(text),
407        _ => Err(ProtocolError::malformed(format!(
408            "expected a number, found {}",
409            format_link(value)
410        ))),
411    }
412}
413
414fn parse_text_number(text: &str) -> ProtocolResult<u32> {
415    text.parse()
416        .map_err(|_| ProtocolError::malformed(format!("expected a number, found '{text}'")))
417}
418
419fn number(value: u32) -> LiNo<String> {
420    LiNo::Ref(value.to_string())
421}
422
423fn named(id: &str, values: Vec<LiNo<String>>) -> LiNo<String> {
424    LiNo::Link {
425        id: Some(id.to_string()),
426        values,
427    }
428}
429
430fn group(values: Vec<LiNo<String>>) -> LiNo<String> {
431    LiNo::Link { id: None, values }
432}
433
434fn malformed_argument(expected: &str, argument: &LiNo<String>) -> ProtocolError {
435    ProtocolError::malformed(format!(
436        "expected {expected}, found {}",
437        format_link(argument)
438    ))
439}
440
441fn malformed_reply(expected: &str, document: &[LiNo<String>]) -> ProtocolError {
442    ProtocolError::malformed(format!(
443        "expected a {expected} reply, found {}",
444        format_document(document)
445    ))
446}