1use 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
11pub trait Queryable {
13 fn supports_field(field: &FieldName) -> bool;
15
16 fn field_value(&self, field: &FieldName) -> SoapResult<ScalarValue>;
22}
23
24#[derive(Debug)]
26pub struct MemoryRepository<E> {
27 entities: RwLock<Vec<E>>,
28}
29
30impl<E> MemoryRepository<E> {
31 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>(¶ms)?;
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) = ¶ms.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) = ¶ms.condition {
255 validate_condition_fields::<E>(condition)?;
256 }
257 for sort in ¶ms.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}