what-core 1.7.0

Core framework for What - an HTML-first web framework powered by Rust
Documentation
//! Data Store - Central state management for What framework
//!
//! Provides an in-memory data store with optional persistence,
//! independent of external APIs/backends.

use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;

use crate::Result;

/// Central data store for the application
#[derive(Clone)]
pub struct DataStore {
    /// In-memory store with RwLock for concurrent access
    inner: Arc<RwLock<StoreInner>>,
    /// Optional file path for persistence
    persistence_path: Option<String>,
}

#[derive(Default, Serialize, Deserialize)]
struct StoreInner {
    /// Collections of data (e.g., "users", "posts")
    collections: HashMap<String, Vec<Value>>,
    /// Key-value store for single values
    values: HashMap<String, Value>,
}

impl DataStore {
    /// Create a new empty data store
    pub fn new() -> Self {
        Self {
            inner: Arc::new(RwLock::new(StoreInner::default())),
            persistence_path: None,
        }
    }

    /// Create a data store with persistence to a JSON file
    pub fn with_persistence(path: impl Into<String>) -> Result<Self> {
        let path = path.into();
        let inner = if Path::new(&path).exists() {
            let content = std::fs::read_to_string(&path)?;
            serde_json::from_str(&content)?
        } else {
            StoreInner::default()
        };

        Ok(Self {
            inner: Arc::new(RwLock::new(inner)),
            persistence_path: Some(path),
        })
    }

    /// Load initial data from a JSON file into a collection
    pub async fn load_collection(&self, name: &str, path: impl AsRef<Path>) -> Result<()> {
        let content = std::fs::read_to_string(path)?;
        let data: Vec<Value> = serde_json::from_str(&content)?;

        let mut inner = self.inner.write().await;
        inner.collections.insert(name.to_string(), data);

        Ok(())
    }

    /// Get all items from a collection
    pub async fn get_collection(&self, name: &str) -> Option<Vec<Value>> {
        let inner = self.inner.read().await;
        inner.collections.get(name).cloned()
    }

    /// Get a single item from a collection by index
    pub async fn get_item(&self, collection: &str, index: usize) -> Option<Value> {
        let inner = self.inner.read().await;
        inner.collections.get(collection)?.get(index).cloned()
    }

    /// Find items in a collection by a field value
    pub async fn find_by(&self, collection: &str, field: &str, value: &Value) -> Vec<Value> {
        let inner = self.inner.read().await;
        inner
            .collections
            .get(collection)
            .map(|items| {
                items
                    .iter()
                    .filter(|item| item.get(field).map(|v| v == value).unwrap_or(false))
                    .cloned()
                    .collect()
            })
            .unwrap_or_default()
    }

    /// Find a single item by a field value
    pub async fn find_one_by(&self, collection: &str, field: &str, value: &Value) -> Option<Value> {
        self.find_by(collection, field, value)
            .await
            .into_iter()
            .next()
    }

    /// Add an item to a collection (CREATE)
    pub async fn create(&self, collection: &str, mut item: Value) -> Result<Value> {
        let mut inner = self.inner.write().await;

        let items = inner
            .collections
            .entry(collection.to_string())
            .or_insert_with(Vec::new);

        // Auto-generate ID if not present
        if item.get("id").is_none() {
            let max_id = items
                .iter()
                .filter_map(|i| i.get("id").and_then(|v| v.as_u64()))
                .max()
                .unwrap_or(0);
            let id = max_id + 1;
            if let Value::Object(ref mut map) = item {
                map.insert("id".to_string(), Value::Number(id.into()));
            }
        }

        // Auto-stamp a sortable creation timestamp so `sort=created_at:desc`
        // works out of the box (millisecond precision avoids ties).
        if let Value::Object(ref mut map) = item {
            map.entry("created_at".to_string()).or_insert_with(|| {
                Value::String(
                    chrono::Local::now()
                        .format("%Y-%m-%dT%H:%M:%S%.3f")
                        .to_string(),
                )
            });
        }

        items.push(item.clone());
        drop(inner);

        self.persist().await?;
        Ok(item)
    }

    /// Update an item in a collection (UPDATE)
    pub async fn update(
        &self,
        collection: &str,
        id: &Value,
        updates: Value,
    ) -> Result<Option<Value>> {
        let mut inner = self.inner.write().await;

        if let Some(items) = inner.collections.get_mut(collection) {
            for item in items.iter_mut() {
                if item.get("id") == Some(id) {
                    // Merge updates into existing item
                    if let (Value::Object(existing), Value::Object(new)) = (item, &updates) {
                        for (key, value) in new {
                            existing.insert(key.clone(), value.clone());
                        }
                    }
                    let updated = items.iter().find(|i| i.get("id") == Some(id)).cloned();
                    drop(inner);
                    self.persist().await?;
                    return Ok(updated);
                }
            }
        }

        Ok(None)
    }

    /// Delete an item from a collection (DELETE)
    pub async fn delete(&self, collection: &str, id: &Value) -> Result<bool> {
        let mut inner = self.inner.write().await;

        if let Some(items) = inner.collections.get_mut(collection) {
            let original_len = items.len();
            items.retain(|item| item.get("id") != Some(id));
            let deleted = items.len() < original_len;

            drop(inner);
            if deleted {
                self.persist().await?;
            }
            return Ok(deleted);
        }

        Ok(false)
    }

    /// Set a single value in the store
    pub async fn set(&self, key: &str, value: Value) -> Result<()> {
        let mut inner = self.inner.write().await;
        inner.values.insert(key.to_string(), value);
        drop(inner);
        self.persist().await
    }

    /// Get a single value from the store
    pub async fn get(&self, key: &str) -> Option<Value> {
        let inner = self.inner.read().await;
        inner.values.get(key).cloned()
    }

    /// Atomically read-modify-write a value in the store.
    /// The closure receives the current value (or None) and returns the new value.
    /// The write lock is held for the entire operation, preventing race conditions.
    pub async fn atomic_modify<F>(&self, key: &str, f: F) -> Result<Value>
    where
        F: FnOnce(Option<&Value>) -> Value,
    {
        let mut inner = self.inner.write().await;
        let new_value = f(inner.values.get(key));
        inner.values.insert(key.to_string(), new_value.clone());
        drop(inner);
        self.persist().await?;
        Ok(new_value)
    }

    /// Delete a single value from the store
    pub async fn remove(&self, key: &str) -> Result<Option<Value>> {
        let mut inner = self.inner.write().await;
        let removed = inner.values.remove(key);
        drop(inner);
        self.persist().await?;
        Ok(removed)
    }

    /// Get all store data as a context for template rendering
    pub async fn as_context(&self) -> HashMap<String, Value> {
        let inner = self.inner.read().await;
        let mut context = HashMap::new();

        // Add collections
        for (name, items) in &inner.collections {
            context.insert(name.clone(), Value::Array(items.clone()));
        }

        // Add values
        for (key, value) in &inner.values {
            context.insert(key.clone(), value.clone());
        }

        context
    }

    /// Replace an entire collection (useful for file/remote data sources)
    pub async fn set_collection(&self, name: &str, items: Vec<Value>) -> Result<()> {
        let mut inner = self.inner.write().await;
        inner.collections.insert(name.to_string(), items);
        drop(inner);
        self.persist().await
    }

    /// Persist store to file if persistence is enabled
    async fn persist(&self) -> Result<()> {
        if let Some(ref path) = self.persistence_path {
            let inner = self.inner.read().await;
            let content = serde_json::to_string_pretty(&*inner)?;
            // Atomic write: write to tmp file, then rename (prevents corruption on crash)
            let tmp_path = format!("{}.tmp", path);
            tokio::fs::write(&tmp_path, &content).await?;
            tokio::fs::rename(&tmp_path, path).await?;
        }
        Ok(())
    }
}

impl Default for DataStore {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[tokio::test]
    async fn test_create_and_get() {
        let store = DataStore::new();

        let post = json!({
            "title": "Hello World",
            "content": "My first post"
        });

        let created = store.create("posts", post).await.unwrap();
        assert_eq!(created.get("id"), Some(&json!(1)));

        let posts = store.get_collection("posts").await.unwrap();
        assert_eq!(posts.len(), 1);
    }

    #[tokio::test]
    async fn test_find_by() {
        let store = DataStore::new();

        store
            .create("users", json!({"name": "Alice", "role": "admin"}))
            .await
            .unwrap();
        store
            .create("users", json!({"name": "Bob", "role": "user"}))
            .await
            .unwrap();
        store
            .create("users", json!({"name": "Charlie", "role": "admin"}))
            .await
            .unwrap();

        let admins = store.find_by("users", "role", &json!("admin")).await;
        assert_eq!(admins.len(), 2);
    }

    #[tokio::test]
    async fn test_update() {
        let store = DataStore::new();

        store
            .create("posts", json!({"title": "Draft"}))
            .await
            .unwrap();

        store
            .update(
                "posts",
                &json!(1),
                json!({"title": "Published", "status": "live"}),
            )
            .await
            .unwrap();

        let post = store.find_one_by("posts", "id", &json!(1)).await.unwrap();
        assert_eq!(post.get("title"), Some(&json!("Published")));
        assert_eq!(post.get("status"), Some(&json!("live")));
    }

    #[tokio::test]
    async fn test_delete() {
        let store = DataStore::new();

        store
            .create("posts", json!({"title": "To Delete"}))
            .await
            .unwrap();

        let deleted = store.delete("posts", &json!(1)).await.unwrap();
        assert!(deleted);

        let posts = store.get_collection("posts").await.unwrap();
        assert!(posts.is_empty());
    }
}