Skip to main content

soaprs_memory/
lib.rs

1//! Reference in-memory implementation of the soaprs repository ports.
2
3use std::{cmp::Ordering, sync::RwLock};
4
5use soaprs_core::{BoxFuture, Entity, SoapError, SoapResult};
6use soaprs_repository::{
7    Condition, FieldName, FindParams, LogicalOperator, Operator, ReadRepository, ScalarValue, Sort,
8    SortDirection, WriteRepository,
9};
10
11/// Provides logical field values to the in-memory query evaluator.
12pub trait Queryable {
13    /// Reports whether the entity exposes this logical query field.
14    fn supports_field(field: &FieldName) -> bool;
15
16    /// Returns the value of a supported logical field.
17    ///
18    /// Nullable fields return [`ScalarValue::Null`]. An unknown field must
19    /// return [`SoapError::validation`], even though the repository validates
20    /// field support before evaluating entities.
21    fn field_value(&self, field: &FieldName) -> SoapResult<ScalarValue>;
22}
23
24/// Thread-safe in-memory repository used as a reference adapter.
25#[derive(Debug)]
26pub struct MemoryRepository<E> {
27    entities: RwLock<Vec<E>>,
28}
29
30impl<E> MemoryRepository<E> {
31    /// Creates an empty repository.
32    pub const fn new() -> Self {
33        Self {
34            entities: RwLock::new(Vec::new()),
35        }
36    }
37
38    fn read_entities(&self) -> SoapResult<std::sync::RwLockReadGuard<'_, Vec<E>>> {
39        self.entities
40            .read()
41            .map_err(|_| SoapError::infrastructure("in-memory repository read lock poisoned"))
42    }
43
44    fn write_entities(&self) -> SoapResult<std::sync::RwLockWriteGuard<'_, Vec<E>>> {
45        self.entities
46            .write()
47            .map_err(|_| SoapError::infrastructure("in-memory repository write lock poisoned"))
48    }
49}
50
51impl<E> Default for MemoryRepository<E> {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl<E> ReadRepository<E> for MemoryRepository<E>
58where
59    E: Entity + Queryable + Clone + 'static,
60{
61    fn find(&self, params: FindParams) -> BoxFuture<'_, SoapResult<Vec<E>>> {
62        Box::pin(async move {
63            params.validate()?;
64            validate_find_fields::<E>(&params)?;
65            let entities = self.read_entities()?;
66            let mut matched = entities
67                .iter()
68                .filter_map(
69                    |entity| match matches_condition(entity, params.condition.as_ref()) {
70                        Ok(true) => Some(Ok(entity.clone())),
71                        Ok(false) => None,
72                        Err(error) => Some(Err(error)),
73                    },
74                )
75                .collect::<SoapResult<Vec<_>>>()?;
76
77            for sort in params.sort.iter().rev() {
78                sort_entities(&mut matched, sort)?;
79            }
80
81            let available = matched.len().saturating_sub(params.offset);
82            let take = params.limit.unwrap_or(available);
83            Ok(matched.into_iter().skip(params.offset).take(take).collect())
84        })
85    }
86
87    fn get<'a>(&'a self, id: &'a E::Id) -> BoxFuture<'a, SoapResult<Option<E>>> {
88        Box::pin(async move {
89            let entities = self.read_entities()?;
90            Ok(entities.iter().find(|entity| entity.id() == id).cloned())
91        })
92    }
93
94    fn count(&self, params: FindParams) -> BoxFuture<'_, SoapResult<u64>> {
95        Box::pin(async move {
96            params.validate()?;
97            if let Some(condition) = &params.condition {
98                validate_condition_fields::<E>(condition)?;
99            }
100            let entities = self.read_entities()?;
101            let mut count = 0_u64;
102            for entity in entities.iter() {
103                if matches_condition(entity, params.condition.as_ref())? {
104                    count = count.saturating_add(1);
105                }
106            }
107            Ok(count)
108        })
109    }
110}
111
112impl<E> WriteRepository<E> for MemoryRepository<E>
113where
114    E: Entity + Queryable + Clone + 'static,
115{
116    fn insert(&self, entity: E) -> BoxFuture<'_, SoapResult<()>> {
117        Box::pin(async move {
118            let mut entities = self.write_entities()?;
119            if entities.iter().any(|existing| existing.id() == entity.id()) {
120                return Err(SoapError::conflict("duplicate entity identifier"));
121            }
122            entities.push(entity);
123            Ok(())
124        })
125    }
126
127    fn replace(&self, entity: E) -> BoxFuture<'_, SoapResult<()>> {
128        Box::pin(async move {
129            let mut entities = self.write_entities()?;
130            let Some(index) = entities
131                .iter()
132                .position(|existing| existing.id() == entity.id())
133            else {
134                return Err(SoapError::not_found("entity identifier"));
135            };
136            entities[index] = entity;
137            Ok(())
138        })
139    }
140
141    fn remove<'a>(&'a self, id: &'a E::Id) -> BoxFuture<'a, SoapResult<bool>> {
142        Box::pin(async move {
143            let mut entities = self.write_entities()?;
144            let Some(index) = entities.iter().position(|entity| entity.id() == id) else {
145                return Ok(false);
146            };
147            entities.remove(index);
148            Ok(true)
149        })
150    }
151}
152
153fn matches_condition<E>(entity: &E, condition: Option<&Condition>) -> SoapResult<bool>
154where
155    E: Queryable,
156{
157    match condition {
158        None => Ok(true),
159        Some(Condition::Group {
160            operator,
161            conditions,
162        }) => match operator {
163            LogicalOperator::And => {
164                for condition in conditions {
165                    if !matches_condition(entity, Some(condition))? {
166                        return Ok(false);
167                    }
168                }
169                Ok(true)
170            }
171            LogicalOperator::Or => {
172                for condition in conditions {
173                    if matches_condition(entity, Some(condition))? {
174                        return Ok(true);
175                    }
176                }
177                Ok(false)
178            }
179        },
180        Some(Condition::Predicate {
181            field,
182            operator,
183            value,
184        }) => evaluate_predicate(entity.field_value(field)?, *operator, value.as_ref()),
185    }
186}
187
188fn evaluate_predicate(
189    actual: ScalarValue,
190    operator: Operator,
191    expected: Option<&ScalarValue>,
192) -> SoapResult<bool> {
193    validate_entity_value(&actual)?;
194    match operator {
195        Operator::IsNull => Ok(matches!(actual, ScalarValue::Null)),
196        Operator::IsNotNull => Ok(!matches!(actual, ScalarValue::Null)),
197        _ if matches!(actual, ScalarValue::Null) => Ok(false),
198        Operator::Eq | Operator::Ne => {
199            let expected = expected
200                .ok_or_else(|| SoapError::validation("equality requires a non-null value"))?;
201            let equivalent = actual
202                .compare(expected)
203                .ok_or_else(|| SoapError::validation("equality requires compatible value types"))?
204                .is_eq();
205            Ok(if operator == Operator::Eq {
206                equivalent
207            } else {
208                !equivalent
209            })
210        }
211        Operator::Gt | Operator::Gte | Operator::Lt | Operator::Lte => {
212            let ordering = comparable_ordering(Some(&actual), expected)?;
213            Ok(match operator {
214                Operator::Gt => ordering == Ordering::Greater,
215                Operator::Gte => ordering != Ordering::Less,
216                Operator::Lt => ordering == Ordering::Less,
217                Operator::Lte => ordering != Ordering::Greater,
218                _ => false,
219            })
220        }
221        Operator::In | Operator::NotIn => {
222            let Some(ScalarValue::List(values)) = expected else {
223                return Err(SoapError::validation("set operators require a list value"));
224            };
225            if let Some(first) = values.first() {
226                if actual.compare(first).is_none() {
227                    return Err(SoapError::validation(
228                        "set membership requires compatible value types",
229                    ));
230                }
231            }
232            let contains = values.iter().any(|item| actual.equivalent(item));
233            Ok(if operator == Operator::In {
234                contains
235            } else {
236                !contains
237            })
238        }
239        Operator::Like => {
240            let (ScalarValue::String(actual), Some(ScalarValue::String(pattern))) =
241                (&actual, expected)
242            else {
243                return Err(SoapError::validation("LIKE requires string values"));
244            };
245            Ok(like_matches(actual, pattern))
246        }
247    }
248}
249
250fn validate_find_fields<E>(params: &FindParams) -> SoapResult<()>
251where
252    E: Queryable,
253{
254    if let Some(condition) = &params.condition {
255        validate_condition_fields::<E>(condition)?;
256    }
257    for sort in &params.sort {
258        validate_field::<E>(&sort.field)?;
259    }
260    Ok(())
261}
262
263fn validate_condition_fields<E>(condition: &Condition) -> SoapResult<()>
264where
265    E: Queryable,
266{
267    match condition {
268        Condition::Predicate { field, .. } => validate_field::<E>(field),
269        Condition::Group { conditions, .. } => {
270            for condition in conditions {
271                validate_condition_fields::<E>(condition)?;
272            }
273            Ok(())
274        }
275    }
276}
277
278fn validate_field<E>(field: &FieldName) -> SoapResult<()>
279where
280    E: Queryable,
281{
282    if E::supports_field(field) {
283        Ok(())
284    } else {
285        Err(SoapError::validation(format!(
286            "unknown query field `{field}`"
287        )))
288    }
289}
290
291fn sort_entities<E>(entities: &mut [E], sort: &Sort) -> SoapResult<()>
292where
293    E: Queryable,
294{
295    for entity in entities.iter() {
296        validate_entity_value(&entity.field_value(&sort.field)?)?;
297    }
298
299    let mut failure = None;
300    entities.sort_by(|left, right| {
301        if failure.is_some() {
302            return Ordering::Equal;
303        }
304
305        match compare_sort_values(
306            left.field_value(&sort.field),
307            right.field_value(&sort.field),
308        ) {
309            Ok(ordering) => match sort.direction {
310                SortDirection::Ascending => ordering,
311                SortDirection::Descending => ordering.reverse(),
312            },
313            Err(error) => {
314                failure = Some(error);
315                Ordering::Equal
316            }
317        }
318    });
319
320    match failure {
321        Some(error) => Err(error),
322        None => Ok(()),
323    }
324}
325
326fn compare_sort_values(
327    left: SoapResult<ScalarValue>,
328    right: SoapResult<ScalarValue>,
329) -> SoapResult<Ordering> {
330    let left = left?;
331    let right = right?;
332    validate_entity_value(&left)?;
333    validate_entity_value(&right)?;
334
335    match (&left, &right) {
336        (ScalarValue::Null, ScalarValue::Null) => Ok(Ordering::Equal),
337        (ScalarValue::Null, _) => Ok(Ordering::Less),
338        (_, ScalarValue::Null) => Ok(Ordering::Greater),
339        _ => left
340            .compare(&right)
341            .ok_or_else(|| SoapError::validation("sorting requires compatible field value types")),
342    }
343}
344
345fn validate_entity_value(value: &ScalarValue) -> SoapResult<()> {
346    value.validate()?;
347    if matches!(value, ScalarValue::List(_)) {
348        Err(SoapError::validation(
349            "entity query fields must contain scalar values",
350        ))
351    } else {
352        Ok(())
353    }
354}
355
356fn comparable_ordering(
357    actual: Option<&ScalarValue>,
358    expected: Option<&ScalarValue>,
359) -> SoapResult<Ordering> {
360    let (Some(actual), Some(expected)) = (actual, expected) else {
361        return Err(SoapError::validation(
362            "comparison requires two non-null values",
363        ));
364    };
365    actual
366        .compare(expected)
367        .ok_or_else(|| SoapError::validation("comparison requires compatible value types"))
368}
369
370fn like_matches(value: &str, pattern: &str) -> bool {
371    let value: Vec<_> = value.chars().collect();
372    let pattern: Vec<_> = pattern.chars().collect();
373    let mut table = vec![vec![false; pattern.len() + 1]; value.len() + 1];
374    table[0][0] = true;
375    for pattern_index in 1..=pattern.len() {
376        if pattern[pattern_index - 1] == '%' {
377            table[0][pattern_index] = table[0][pattern_index - 1];
378        }
379    }
380    for value_index in 1..=value.len() {
381        for pattern_index in 1..=pattern.len() {
382            table[value_index][pattern_index] = match pattern[pattern_index - 1] {
383                '%' => {
384                    table[value_index][pattern_index - 1] || table[value_index - 1][pattern_index]
385                }
386                '_' => table[value_index - 1][pattern_index - 1],
387                character => {
388                    character == value[value_index - 1] && table[value_index - 1][pattern_index - 1]
389                }
390            };
391        }
392    }
393    table[value.len()][pattern.len()]
394}
395
396#[cfg(test)]
397mod tests {
398    use std::cmp::Ordering;
399
400    use soaprs_core::{SoapError, SoapErrorKind};
401    use soaprs_repository::{Operator, ScalarValue};
402
403    use super::{comparable_ordering, evaluate_predicate, like_matches};
404
405    #[test]
406    fn evaluates_all_comparison_operators() {
407        let actual = ScalarValue::I64(10);
408
409        assert_eq!(
410            evaluate_predicate(actual.clone(), Operator::Eq, Some(&ScalarValue::U64(10))).ok(),
411            Some(true)
412        );
413        assert_eq!(
414            evaluate_predicate(actual.clone(), Operator::Ne, Some(&ScalarValue::I64(11))).ok(),
415            Some(true)
416        );
417        assert_eq!(
418            evaluate_predicate(actual.clone(), Operator::Gt, Some(&ScalarValue::I64(9))).ok(),
419            Some(true)
420        );
421        assert_eq!(
422            evaluate_predicate(actual.clone(), Operator::Gte, Some(&ScalarValue::I64(10))).ok(),
423            Some(true)
424        );
425        assert_eq!(
426            evaluate_predicate(actual.clone(), Operator::Lt, Some(&ScalarValue::I64(11))).ok(),
427            Some(true)
428        );
429        assert_eq!(
430            evaluate_predicate(actual, Operator::Lte, Some(&ScalarValue::F64(10.0))).ok(),
431            Some(true)
432        );
433    }
434
435    #[test]
436    fn evaluates_set_and_null_operators() {
437        let values = ScalarValue::List(vec![ScalarValue::I64(1), ScalarValue::U64(2)]);
438
439        assert_eq!(
440            evaluate_predicate(ScalarValue::I64(2), Operator::In, Some(&values)).ok(),
441            Some(true)
442        );
443        assert_eq!(
444            evaluate_predicate(ScalarValue::I64(3), Operator::NotIn, Some(&values)).ok(),
445            Some(true)
446        );
447        assert_eq!(
448            evaluate_predicate(ScalarValue::Null, Operator::IsNull, None).ok(),
449            Some(true)
450        );
451        assert_eq!(
452            evaluate_predicate(ScalarValue::Null, Operator::IsNotNull, None).ok(),
453            Some(false)
454        );
455        assert_eq!(
456            evaluate_predicate(ScalarValue::Null, Operator::Ne, Some(&ScalarValue::I64(1))).ok(),
457            Some(false)
458        );
459    }
460
461    #[test]
462    fn like_uses_characters_instead_of_utf8_bytes() {
463        assert!(like_matches("Łódź", "_ód%"));
464        assert!(!like_matches("Łódź", "__ód%"));
465        assert!(like_matches(r"a\b", r"a\b"));
466    }
467
468    #[test]
469    fn incompatible_comparisons_return_validation_errors() {
470        let result = comparable_ordering(
471            Some(&ScalarValue::String("10".into())),
472            Some(&ScalarValue::I64(10)),
473        );
474
475        assert_eq!(
476            result.as_ref().map_err(SoapError::kind),
477            Err(SoapErrorKind::Validation)
478        );
479        assert_ne!(result.ok(), Some(Ordering::Equal));
480    }
481}