Skip to main content

lino_rest_api/
store.rs

1//! The store interface a resource is built on, and an in-memory implementation.
2//!
3//! A store is anything that can list, read, create, replace, merge and delete
4//! items; [`MemoryStore`] is the built-in one, so that a working CRUD API is one
5//! call away in examples, tests and prototypes.
6
7use std::sync::Mutex;
8
9use async_trait::async_trait;
10use lino_objects_codec::LinoValue;
11
12use crate::collection::apply_collection_query;
13use crate::problem::LinoHttpError;
14use crate::query::CollectionQuery;
15use crate::value::{as_object, get, merge, set};
16
17/// What a store must be able to do for [`crate::resource::register_resource`].
18///
19/// Every method is asynchronous, so a store may read from a database, and every
20/// one may fail with a [`LinoHttpError`], so a store may refuse a write with any
21/// status it likes.
22#[async_trait]
23pub trait Store: Send + Sync {
24    /// The field of an item that holds its identifier.
25    fn id_field(&self) -> &str {
26        "id"
27    }
28
29    /// Run a collection query, returning a collection envelope.
30    async fn list(&self, query: &CollectionQuery) -> Result<LinoValue, LinoHttpError>;
31
32    /// Read one item, or [`None`] when it does not exist.
33    async fn get(&self, identifier: &str) -> Result<Option<LinoValue>, LinoHttpError>;
34
35    /// Create an item, assigning an identifier when the body carries none.
36    async fn create(&self, body: &LinoValue) -> Result<LinoValue, LinoHttpError>;
37
38    /// Replace an item, or [`None`] when it does not exist.
39    async fn update(
40        &self,
41        identifier: &str,
42        body: &LinoValue,
43    ) -> Result<Option<LinoValue>, LinoHttpError>;
44
45    /// Merge changes into an item, or [`None`] when it does not exist.
46    async fn patch(
47        &self,
48        identifier: &str,
49        body: &LinoValue,
50    ) -> Result<Option<LinoValue>, LinoHttpError>;
51
52    /// Delete an item, reporting whether one was deleted.
53    async fn remove(&self, identifier: &str) -> Result<bool, LinoHttpError>;
54}
55
56/// Normalise an identifier so that `"1"` from a path matches a stored `1`.
57///
58/// # Examples
59///
60/// ```
61/// use lino_objects_codec::LinoValue;
62/// use lino_rest_api::store::normalize_id;
63///
64/// assert_eq!(normalize_id("1"), LinoValue::Int(1));
65/// assert_eq!(normalize_id("abc"), LinoValue::String("abc".into()));
66/// ```
67pub fn normalize_id(identifier: &str) -> LinoValue {
68    match identifier.parse::<i64>() {
69        Ok(number) if !identifier.is_empty() => LinoValue::Int(number),
70        _ => LinoValue::String(identifier.to_string()),
71    }
72}
73
74/// Normalise an identifier that is already a value.
75fn normalize_value_id(identifier: &LinoValue) -> LinoValue {
76    match identifier {
77        LinoValue::String(text) => normalize_id(text),
78        other => other.clone(),
79    }
80}
81
82/// Render an identifier as the text a path carries.
83pub fn id_to_string(identifier: &LinoValue) -> String {
84    crate::value::to_query_text(identifier)
85}
86
87/// A resource store backed by an ordered list of items.
88///
89/// Items keep the order they were created in, which is the order the collection
90/// lists them in when no `sort` is asked for.
91#[derive(Debug, Default)]
92pub struct MemoryStore {
93    id_field: String,
94    state: Mutex<MemoryState>,
95}
96
97#[derive(Debug, Default)]
98struct MemoryState {
99    items: Vec<(LinoValue, LinoValue)>,
100    next_id: i64,
101}
102
103impl MemoryStore {
104    /// An empty store whose identifier field is `id`.
105    pub fn new() -> Self {
106        Self {
107            id_field: "id".to_string(),
108            state: Mutex::new(MemoryState {
109                items: Vec::new(),
110                next_id: 1,
111            }),
112        }
113    }
114
115    /// An empty store whose identifier field has another name.
116    pub fn with_id_field<S: Into<String>>(id_field: S) -> Self {
117        Self {
118            id_field: id_field.into(),
119            ..Self::new()
120        }
121    }
122
123    /// A store seeded with items.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use lino_rest_api::store::MemoryStore;
129    /// use lino_rest_api::value::{get, int, object, string};
130    ///
131    /// let store = MemoryStore::seeded([object([("title", string("Ship it"))])]);
132    /// assert_eq!(get(&store.read("1").unwrap(), "id"), Some(&int(1)));
133    /// ```
134    pub fn seeded<I: IntoIterator<Item = LinoValue>>(items: I) -> Self {
135        let store = Self::new();
136        for item in items {
137            store.insert(&item);
138        }
139        store
140    }
141
142    /// Create an item without awaiting, for seeding and for tests.
143    pub fn insert(&self, body: &LinoValue) -> LinoValue {
144        let mut state = self.state.lock().expect("store is not poisoned");
145        let identifier = match get(body, &self.id_field) {
146            Some(LinoValue::Null) | None => {
147                let identifier = LinoValue::Int(state.next_id);
148                state.next_id += 1;
149                identifier
150            }
151            Some(provided) => normalize_value_id(provided),
152        };
153        if let LinoValue::Int(number) = identifier {
154            if number >= state.next_id {
155                state.next_id = number + 1;
156            }
157        }
158
159        let mut item = if matches!(body, LinoValue::Object(_)) {
160            body.clone()
161        } else {
162            LinoValue::Object(Vec::new())
163        };
164        set(&mut item, self.id_field.clone(), identifier.clone());
165
166        match state.items.iter_mut().find(|(key, _)| *key == identifier) {
167            Some(entry) => entry.1 = item.clone(),
168            None => state.items.push((identifier, item.clone())),
169        }
170        item
171    }
172
173    /// Read an item without awaiting, for tests.
174    pub fn read(&self, identifier: &str) -> Option<LinoValue> {
175        let key = normalize_id(identifier);
176        let state = self.state.lock().expect("store is not poisoned");
177        state
178            .items
179            .iter()
180            .find(|(existing, _)| *existing == key)
181            .map(|(_, item)| item.clone())
182    }
183
184    /// Every item, in creation order.
185    pub fn items(&self) -> Vec<LinoValue> {
186        let state = self.state.lock().expect("store is not poisoned");
187        state.items.iter().map(|(_, item)| item.clone()).collect()
188    }
189
190    /// How many items the store holds.
191    pub fn len(&self) -> usize {
192        self.state
193            .lock()
194            .expect("store is not poisoned")
195            .items
196            .len()
197    }
198
199    /// Whether the store holds no item.
200    pub fn is_empty(&self) -> bool {
201        self.len() == 0
202    }
203
204    /// Remove every item and restart the identifier sequence.
205    pub fn clear(&self) {
206        let mut state = self.state.lock().expect("store is not poisoned");
207        state.items.clear();
208        state.next_id = 1;
209    }
210
211    /// Replace the item at an identifier, or report that there is none.
212    fn replace(&self, identifier: &str, item: LinoValue) -> Option<LinoValue> {
213        let key = normalize_id(identifier);
214        let mut state = self.state.lock().expect("store is not poisoned");
215        let entry = state
216            .items
217            .iter_mut()
218            .find(|(existing, _)| *existing == key)?;
219        entry.1 = item.clone();
220        Some(item)
221    }
222}
223
224#[async_trait]
225impl Store for MemoryStore {
226    fn id_field(&self) -> &str {
227        &self.id_field
228    }
229
230    async fn list(&self, query: &CollectionQuery) -> Result<LinoValue, LinoHttpError> {
231        Ok(apply_collection_query(&self.items(), query))
232    }
233
234    async fn get(&self, identifier: &str) -> Result<Option<LinoValue>, LinoHttpError> {
235        Ok(self.read(identifier))
236    }
237
238    async fn create(&self, body: &LinoValue) -> Result<LinoValue, LinoHttpError> {
239        Ok(self.insert(body))
240    }
241
242    async fn update(
243        &self,
244        identifier: &str,
245        body: &LinoValue,
246    ) -> Result<Option<LinoValue>, LinoHttpError> {
247        if self.read(identifier).is_none() {
248            return Ok(None);
249        }
250        let mut item = if as_object(body).is_some() {
251            body.clone()
252        } else {
253            LinoValue::Object(Vec::new())
254        };
255        set(&mut item, self.id_field.clone(), normalize_id(identifier));
256        Ok(self.replace(identifier, item))
257    }
258
259    async fn patch(
260        &self,
261        identifier: &str,
262        body: &LinoValue,
263    ) -> Result<Option<LinoValue>, LinoHttpError> {
264        let Some(existing) = self.read(identifier) else {
265            return Ok(None);
266        };
267        let mut item = merge(&existing, body);
268        set(&mut item, self.id_field.clone(), normalize_id(identifier));
269        Ok(self.replace(identifier, item))
270    }
271
272    async fn remove(&self, identifier: &str) -> Result<bool, LinoHttpError> {
273        let key = normalize_id(identifier);
274        let mut state = self.state.lock().expect("store is not poisoned");
275        match state
276            .items
277            .iter()
278            .position(|(existing, _)| *existing == key)
279        {
280            Some(index) => {
281                state.items.remove(index);
282                Ok(true)
283            }
284            None => Ok(false),
285        }
286    }
287}