xz-memory-engine 0.2.0

Reusable engine implementations for xz-memory-core: storage backends and layered memory
Documentation
use std::sync::Arc;

use serde::{Deserialize, Serialize};

use xz_memory_core::StoreError;
use xz_memory_core::traits::store::EntryStore;

use crate::backends::InMemoryEntryStore;

#[cfg(feature = "sqlite-backend")]
use crate::backends::SqliteEntryStore;

#[cfg(feature = "markdown-backend")]
use crate::backends::MarkdownEntryStore;

/// Configuration for the memory system.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MemoryConfig {
    pub storage: StorageConfig,
}

impl MemoryConfig {
    /// Build an [`EntryStore`] from this configuration.
    ///
    /// # Errors
    ///
    /// Returns [`StoreError::Config`] if the `backend` string is unrecognised.
    /// Returns [`StoreError::Backend`] if the backend fails to initialise (e.g. SQLite connection error).
    pub async fn build(&self) -> Result<Arc<dyn EntryStore>, StoreError> {
        match self.storage.backend.as_str() {
            "memory" => Ok(Arc::new(InMemoryEntryStore::new())),
            #[cfg(feature = "sqlite-backend")]
            "sqlite" => Ok(Arc::new(SqliteEntryStore::new(&self.storage.path).await?)),
            #[cfg(feature = "markdown-backend")]
            "markdown" => {
                Ok(Arc::new(MarkdownEntryStore::new(std::path::PathBuf::from(&self.storage.path))))
            }
            unknown => Err(StoreError::Config(format!("Unknown backend: {}", unknown))),
        }
    }
}

/// Storage backend configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
    /// Backend type identifier (e.g. `"sqlite"`, `"memory"`).
    pub backend: String,
    /// Path to the backend resource (file path, directory, or connection string).
    pub path: String,
    /// Number of connections in the backend connection pool.
    pub pool_size: u32,
}

impl Default for StorageConfig {
    fn default() -> Self {
        Self { backend: "sqlite".into(), path: "./data/memory.db".into(), pool_size: 5 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn build_memory_backend() {
        let config = MemoryConfig {
            storage: StorageConfig { backend: "memory".into(), path: String::new(), pool_size: 1 },
        };
        let store = config.build().await.unwrap();
        let entry = xz_memory_core::types::entry::Entry {
            id: "test-1".into(),
            partition: "p".into(),
            body: "hello".into(),
            recorded_at: 100,
        };
        store.append(entry).await.unwrap();
        let results = store
            .query(
                "p",
                &xz_memory_core::types::entry::TimeRange { start: None, end: None },
                &xz_memory_core::types::entry::QueryOptions {
                    limit: 10,
                    sort: xz_memory_core::types::entry::SortOrder::Ascending,
                },
            )
            .await
            .unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].body, "hello");
    }

    #[cfg(feature = "sqlite-backend")]
    #[tokio::test]
    async fn build_sqlite_backend() {
        let config = MemoryConfig {
            storage: StorageConfig {
                backend: "sqlite".into(),
                path: "sqlite::memory:".into(),
                pool_size: 1,
            },
        };
        let store = config.build().await.unwrap();
        let entry = xz_memory_core::types::entry::Entry {
            id: "sqlite-test-1".into(),
            partition: "p".into(),
            body: "from sqlite".into(),
            recorded_at: 200,
        };
        store.append(entry).await.unwrap();
        let results = store
            .query(
                "p",
                &xz_memory_core::types::entry::TimeRange { start: None, end: None },
                &xz_memory_core::types::entry::QueryOptions {
                    limit: 10,
                    sort: xz_memory_core::types::entry::SortOrder::Ascending,
                },
            )
            .await
            .unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].body, "from sqlite");
    }

    #[tokio::test]
    async fn build_unknown_backend_returns_config_error() {
        let config = MemoryConfig {
            storage: StorageConfig {
                backend: "postgres".into(),
                path: String::new(),
                pool_size: 1,
            },
        };
        match config.build().await {
            Err(e) => {
                assert!(matches!(e, StoreError::Config(_)));
                assert!(e.to_string().contains("postgres"));
            }
            Ok(_) => panic!("expected Err, got Ok"),
        }
    }
}