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