Skip to main content

meta_language/
storage.rs

1//! Pluggable storage backends for meta-language links.
2//!
3//! [`LinkStore`] is the storage boundary for the links network: reads use
4//! `&self`, writes use `&mut self`, and the default implementation is the
5//! existing in-memory [`LinkNetwork`](crate::LinkNetwork). The optional
6//! `doublets` Cargo feature adds a file-mapped binary backend over the
7//! `doublets` crate.
8
9use std::error::Error;
10use std::fmt;
11use std::sync::Arc;
12
13use crate::access::{EngineNetwork, ReadOnlyNetwork, ReadOnlyViolation};
14use crate::configuration::AccessMode;
15use crate::link_network::{Link, LinkId, LinkMetadata, LinkNetwork, LinkType};
16
17/// Storage-level errors returned by [`LinkStore`] implementations.
18#[derive(Debug)]
19pub enum StorageError {
20    /// A write was attempted through a read-only storage handle.
21    ReadOnly(ReadOnlyViolation),
22    /// File I/O failed while opening or maintaining a file-backed store.
23    Io(std::io::Error),
24    /// The optional `doublets` backend returned an error.
25    Doublets(String),
26    /// Stored bytes do not match the meta-language storage schema.
27    Corrupt(String),
28}
29
30impl fmt::Display for StorageError {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::ReadOnly(error) => error.fmt(formatter),
34            Self::Io(error) => write!(formatter, "storage I/O error: {error}"),
35            Self::Doublets(error) => write!(formatter, "doublets storage error: {error}"),
36            Self::Corrupt(error) => write!(formatter, "corrupt storage: {error}"),
37        }
38    }
39}
40
41impl Error for StorageError {
42    fn source(&self) -> Option<&(dyn Error + 'static)> {
43        match self {
44            Self::ReadOnly(error) => Some(error),
45            Self::Io(error) => Some(error),
46            Self::Doublets(_) | Self::Corrupt(_) => None,
47        }
48    }
49}
50
51impl From<ReadOnlyViolation> for StorageError {
52    fn from(error: ReadOnlyViolation) -> Self {
53        Self::ReadOnly(error)
54    }
55}
56
57impl From<std::io::Error> for StorageError {
58    fn from(error: std::io::Error) -> Self {
59        Self::Io(error)
60    }
61}
62
63/// Storage backend names used by downstream engine configuration.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65pub enum LinkStoreBackend {
66    /// Store and exchange networks through canonical `LiNo` text.
67    LinoProjection,
68    /// Store networks in `doublets-rs` file-mapped binary doublets.
69    DoubletsRs,
70    /// Exchange the same binary layout with a browser-side `doublets-web` host.
71    DoubletsWeb,
72}
73
74impl LinkStoreBackend {
75    /// Human-readable backend label.
76    #[must_use]
77    pub const fn label(self) -> &'static str {
78        match self {
79            Self::LinoProjection => "LiNo projection",
80            Self::DoubletsRs => "doublets-rs",
81            Self::DoubletsWeb => "doublets-web",
82        }
83    }
84}
85
86/// Query used by [`LinkStore::search`] and [`LinkStore::count`].
87#[derive(Clone, Debug, Default, PartialEq, Eq)]
88pub struct LinkStoreQuery {
89    id: Option<LinkId>,
90    references: Option<Vec<LinkId>>,
91    link_type: Option<LinkType>,
92    term: Option<String>,
93    language: Option<String>,
94    named: Option<bool>,
95}
96
97impl LinkStoreQuery {
98    /// Creates a query that matches every link.
99    #[must_use]
100    pub const fn new() -> Self {
101        Self {
102            id: None,
103            references: None,
104            link_type: None,
105            term: None,
106            language: None,
107            named: None,
108        }
109    }
110
111    /// Restricts the query to one link id.
112    #[must_use]
113    pub const fn with_id(mut self, id: LinkId) -> Self {
114        self.id = Some(id);
115        self
116    }
117
118    /// Restricts the query to links with exactly these ordered references.
119    #[must_use]
120    pub fn with_references<I>(mut self, references: I) -> Self
121    where
122        I: IntoIterator<Item = LinkId>,
123    {
124        self.references = Some(references.into_iter().collect());
125        self
126    }
127
128    /// Restricts the query to a link type.
129    #[must_use]
130    pub const fn with_link_type(mut self, link_type: LinkType) -> Self {
131        self.link_type = Some(link_type);
132        self
133    }
134
135    /// Restricts the query to links with this term.
136    #[must_use]
137    pub fn with_term(mut self, term: impl Into<String>) -> Self {
138        self.term = Some(term.into());
139        self
140    }
141
142    /// Restricts the query to links with this language label.
143    #[must_use]
144    pub fn with_language(mut self, language: impl Into<String>) -> Self {
145        self.language = Some(language.into());
146        self
147    }
148
149    /// Restricts the query to named or anonymous links.
150    #[must_use]
151    pub const fn with_named(mut self, named: bool) -> Self {
152        self.named = Some(named);
153        self
154    }
155
156    fn matches(&self, link: &Link) -> bool {
157        if self.id.is_some_and(|id| id != link.id()) {
158            return false;
159        }
160        if self
161            .references
162            .as_deref()
163            .is_some_and(|references| references != link.references())
164        {
165            return false;
166        }
167        if self
168            .link_type
169            .is_some_and(|link_type| Some(link_type) != link.metadata().link_type())
170        {
171            return false;
172        }
173        if self
174            .term
175            .as_deref()
176            .is_some_and(|term| Some(term) != link.metadata().term())
177        {
178            return false;
179        }
180        if self
181            .language
182            .as_deref()
183            .is_some_and(|language| Some(language) != link.metadata().language())
184        {
185            return false;
186        }
187        if self
188            .named
189            .is_some_and(|named| named != link.metadata().is_named())
190        {
191            return false;
192        }
193        true
194    }
195}
196
197/// Storage trait for create/read/update/delete/search over meta-language links.
198pub trait LinkStore {
199    /// Creates a link and returns its stable id.
200    ///
201    /// # Errors
202    ///
203    /// Returns [`StorageError`] when the backend cannot write the link.
204    fn create(
205        &mut self,
206        references: &[LinkId],
207        metadata: LinkMetadata,
208    ) -> Result<LinkId, StorageError>;
209
210    /// Reads a link by id.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`StorageError`] when the backend cannot read its storage.
215    fn read(&self, id: LinkId) -> Result<Option<Link>, StorageError>;
216
217    /// Replaces an existing link's references and metadata.
218    ///
219    /// # Errors
220    ///
221    /// Returns [`StorageError`] when the backend cannot write the update.
222    fn update(
223        &mut self,
224        id: LinkId,
225        references: &[LinkId],
226        metadata: LinkMetadata,
227    ) -> Result<bool, StorageError>;
228
229    /// Deletes a link by id.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`StorageError`] when the backend cannot delete the link.
234    fn delete(&mut self, id: LinkId) -> Result<bool, StorageError>;
235
236    /// Returns links matching `query`.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`StorageError`] when the backend cannot search its storage.
241    fn search(&self, query: &LinkStoreQuery) -> Result<Vec<Link>, StorageError>;
242
243    /// Counts links matching `query`.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`StorageError`] when the backend cannot search its storage.
248    fn count(&self, query: &LinkStoreQuery) -> Result<usize, StorageError> {
249        self.search(query).map(|links| links.len())
250    }
251}
252
253impl LinkStore for LinkNetwork {
254    fn create(
255        &mut self,
256        references: &[LinkId],
257        metadata: LinkMetadata,
258    ) -> Result<LinkId, StorageError> {
259        Ok(self.insert_dynamic_link(references, metadata))
260    }
261
262    fn read(&self, id: LinkId) -> Result<Option<Link>, StorageError> {
263        Ok(self.link(id).cloned())
264    }
265
266    fn update(
267        &mut self,
268        id: LinkId,
269        references: &[LinkId],
270        metadata: LinkMetadata,
271    ) -> Result<bool, StorageError> {
272        Ok(replace_network_link(self, id, references, metadata, true))
273    }
274
275    fn delete(&mut self, id: LinkId) -> Result<bool, StorageError> {
276        Ok(delete_network_link(self, id))
277    }
278
279    fn search(&self, query: &LinkStoreQuery) -> Result<Vec<Link>, StorageError> {
280        Ok(self
281            .links()
282            .filter(|link| query.matches(link))
283            .cloned()
284            .collect())
285    }
286}
287
288fn insert_network_link_with_id(
289    network: &mut LinkNetwork,
290    id: LinkId,
291    references: &[LinkId],
292    metadata: LinkMetadata,
293    registered_term: bool,
294) {
295    let term = registered_term
296        .then(|| metadata.term().map(Arc::<str>::from))
297        .flatten();
298    network.links.insert(
299        id,
300        Arc::new(Link {
301            id,
302            references: Arc::from(references.to_vec()),
303            metadata,
304        }),
305    );
306    if let Some(term) = term {
307        network.terms.insert(term, id);
308    }
309    network.next_id = network.next_id.max(id.as_u64() + 1);
310}
311
312fn replace_network_link(
313    network: &mut LinkNetwork,
314    id: LinkId,
315    references: &[LinkId],
316    metadata: LinkMetadata,
317    registered_term: bool,
318) -> bool {
319    if !network.links.contains_key(&id) {
320        return false;
321    }
322    network.terms.retain(|_, stored_id| *stored_id != id);
323    insert_network_link_with_id(network, id, references, metadata, registered_term);
324    true
325}
326
327fn delete_network_link(network: &mut LinkNetwork, id: LinkId) -> bool {
328    let removed = network.links.remove(&id).is_some();
329    if removed {
330        network.terms.retain(|_, stored_id| *stored_id != id);
331    }
332    removed
333}
334
335#[cfg(feature = "doublets")]
336fn network_from_stored_links(links: Vec<(Link, bool)>) -> LinkNetwork {
337    let mut network = LinkNetwork::new();
338    for (link, registered_term) in links {
339        insert_network_link_with_id(
340            &mut network,
341            link.id,
342            &link.references,
343            link.metadata,
344            registered_term,
345        );
346    }
347    network
348}
349
350impl LinkStore for ReadOnlyNetwork {
351    fn create(
352        &mut self,
353        _references: &[LinkId],
354        _metadata: LinkMetadata,
355    ) -> Result<LinkId, StorageError> {
356        Err(ReadOnlyViolation.into())
357    }
358
359    fn read(&self, id: LinkId) -> Result<Option<Link>, StorageError> {
360        LinkStore::read(self.network(), id)
361    }
362
363    fn update(
364        &mut self,
365        _id: LinkId,
366        _references: &[LinkId],
367        _metadata: LinkMetadata,
368    ) -> Result<bool, StorageError> {
369        Err(ReadOnlyViolation.into())
370    }
371
372    fn delete(&mut self, _id: LinkId) -> Result<bool, StorageError> {
373        Err(ReadOnlyViolation.into())
374    }
375
376    fn search(&self, query: &LinkStoreQuery) -> Result<Vec<Link>, StorageError> {
377        LinkStore::search(self.network(), query)
378    }
379}
380
381impl LinkStore for EngineNetwork {
382    fn create(
383        &mut self,
384        references: &[LinkId],
385        metadata: LinkMetadata,
386    ) -> Result<LinkId, StorageError> {
387        LinkStore::create(self.as_mutable()?, references, metadata)
388    }
389
390    fn read(&self, id: LinkId) -> Result<Option<Link>, StorageError> {
391        LinkStore::read(self.network(), id)
392    }
393
394    fn update(
395        &mut self,
396        id: LinkId,
397        references: &[LinkId],
398        metadata: LinkMetadata,
399    ) -> Result<bool, StorageError> {
400        LinkStore::update(self.as_mutable()?, id, references, metadata)
401    }
402
403    fn delete(&mut self, id: LinkId) -> Result<bool, StorageError> {
404        LinkStore::delete(self.as_mutable()?, id)
405    }
406
407    fn search(&self, query: &LinkStoreQuery) -> Result<Vec<Link>, StorageError> {
408        LinkStore::search(self.network(), query)
409    }
410}
411
412/// Access-mode wrapper for any [`LinkStore`] implementation.
413#[derive(Clone, Debug, PartialEq, Eq)]
414pub enum EngineLinkStore<S> {
415    /// Mutable storage.
416    Mutable(S),
417    /// Read-only storage.
418    ReadOnly(S),
419}
420
421impl<S> EngineLinkStore<S> {
422    /// Wraps a store according to the configured access mode.
423    #[must_use]
424    pub const fn with_access_mode(store: S, access_mode: AccessMode) -> Self {
425        match access_mode {
426            AccessMode::Mutable => Self::Mutable(store),
427            AccessMode::ReadOnly => Self::ReadOnly(store),
428        }
429    }
430
431    /// Returns the configured access mode.
432    #[must_use]
433    pub const fn access_mode(&self) -> AccessMode {
434        match self {
435            Self::Mutable(_) => AccessMode::Mutable,
436            Self::ReadOnly(_) => AccessMode::ReadOnly,
437        }
438    }
439
440    /// Borrows the wrapped store.
441    #[must_use]
442    pub const fn store(&self) -> &S {
443        match self {
444            Self::Mutable(store) | Self::ReadOnly(store) => store,
445        }
446    }
447
448    /// Consumes the wrapper and returns the store.
449    pub fn into_inner(self) -> S {
450        match self {
451            Self::Mutable(store) | Self::ReadOnly(store) => store,
452        }
453    }
454}
455
456impl<S: LinkStore> LinkStore for EngineLinkStore<S> {
457    fn create(
458        &mut self,
459        references: &[LinkId],
460        metadata: LinkMetadata,
461    ) -> Result<LinkId, StorageError> {
462        match self {
463            Self::Mutable(store) => store.create(references, metadata),
464            Self::ReadOnly(_) => Err(ReadOnlyViolation.into()),
465        }
466    }
467
468    fn read(&self, id: LinkId) -> Result<Option<Link>, StorageError> {
469        self.store().read(id)
470    }
471
472    fn update(
473        &mut self,
474        id: LinkId,
475        references: &[LinkId],
476        metadata: LinkMetadata,
477    ) -> Result<bool, StorageError> {
478        match self {
479            Self::Mutable(store) => store.update(id, references, metadata),
480            Self::ReadOnly(_) => Err(ReadOnlyViolation.into()),
481        }
482    }
483
484    fn delete(&mut self, id: LinkId) -> Result<bool, StorageError> {
485        match self {
486            Self::Mutable(store) => store.delete(id),
487            Self::ReadOnly(_) => Err(ReadOnlyViolation.into()),
488        }
489    }
490
491    fn search(&self, query: &LinkStoreQuery) -> Result<Vec<Link>, StorageError> {
492        self.store().search(query)
493    }
494}
495
496#[cfg(feature = "doublets")]
497mod doublets;
498#[cfg(feature = "doublets")]
499pub use doublets::DoubletsLinkStore;