athena_rs 0.77.1

WIP Database API gateway
Documentation
//! ## Cache hydration
//!
//! This module provides functionality to hydrate the application's shared cache with JSON data.
//! It includes a function to insert JSON data into the cache and return the same data.

use crate::AppState;
use actix_web::web::Data;
use serde_json::Value;
// no-op: keep imports minimal here
use crate::utils::redis_client::GLOBAL_REDIS;

/// Hydrates the cache with the provided JSON body and returns the JSON body.
///
/// # Arguments
///
/// * `app_state` - A `Data<AppState>` representing the shared application state with caches.
/// * `cache_key` - A `String` that serves as the key for storing the JSON body in the cache.
/// * `json_body` - A `Vec<Value>` containing the JSON data to be cached.
///
/// # Returns
///
/// * `Vec<Value>` - The same JSON body that was provided as input.
///
/// # Example
///
/// ```rust,no_run
/// # use actix_web::web::Data;
/// # use athena_rs::api::cache::hydrate::hydrate_cache_and_return_json;
/// # use athena_rs::AppState;
/// # use athena_rs::drivers::postgresql::sqlx_driver::PostgresClientRegistry;
/// # use moka::future::Cache;
/// # use reqwest::Client;
/// # use serde_json::json;
/// # use std::sync::Arc;
/// # async fn doc_example() {
/// #     let cache: Cache<String, serde_json::Value> = Cache::builder().build();
/// #     let immortal: Cache<String, serde_json::Value> = Cache::builder().build();
/// #     let app_state = AppState {
/// #         cache: Arc::new(cache),
/// #         immortal_cache: Arc::new(immortal),
/// #         client: Client::new(),
/// #         pg_registry: Arc::new(PostgresClientRegistry::empty()),
/// #         gateway_force_camel_case_to_snake_case: false,
/// #         pipeline_registry: None,
/// #     };
/// #     let app_state = Data::new(app_state);
/// let cache_key = String::from("my_cache_key");
/// let json_body = vec![json!(1), json!(2), json!(3)];
/// let result = hydrate_cache_and_return_json(app_state, cache_key, json_body).await;
/// assert_eq!(result, vec![json!(1), json!(2), json!(3)]);
/// # }
/// ```
pub async fn hydrate_cache_and_return_json(
    app_state: Data<AppState>,
    cache_key: String,
    json_body: Vec<Value>,
) -> Vec<Value> {
    // Store just the array payload in the cache to match consumers' expectations
    let value_to_cache: Value = Value::Array(json_body.clone());
    app_state
        .cache
        .insert(cache_key.clone(), value_to_cache)
        .await;

    // Also write-through to Redis with a TTL that mirrors the in-memory cache TTL if desired
    if let Some(redis) = GLOBAL_REDIS.get() {
        let _ = redis
            .lock()
            .await
            .set_with_ttl(&cache_key, &Value::Array(json_body.clone()), 900u64)
            .await;
    }

    json_body
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::drivers::postgresql::sqlx_driver::PostgresClientRegistry;
    use actix_web::web::Data;
    use moka::future::Cache;
    use reqwest::Client;
    use serde_json::json;
    use std::sync::Arc;

    #[tokio::test]
    async fn test_hydrate_inserts_and_returns() {
        let cache: Cache<String, Value> = Cache::builder().build();
        let immortal: Cache<String, Value> = Cache::builder().build();
        let app: AppState = AppState {
            cache: Arc::new(cache),
            immortal_cache: Arc::new(immortal),
            client: Client::new(),
            pg_registry: Arc::new(PostgresClientRegistry::empty()),
            gateway_force_camel_case_to_snake_case: false,
            pipeline_registry: None,
            logging_client_name: None,
        };
        let data: Data<AppState> = Data::new(app);

        let key: String = "test_key".to_string();
        let payload: Vec<Value> = vec![json!({"a": 1}), json!({"b": 2})];

        let returned: Vec<Value> =
            hydrate_cache_and_return_json(data.clone(), key.clone(), payload.clone()).await;
        assert_eq!(returned, payload);
        let cached: Option<Value> = data.cache.get(&key).await;
        assert!(cached.is_some());
        assert_eq!(cached.unwrap(), Value::Array(payload));
    }

    #[tokio::test]
    async fn test_hydrate_overwrites_existing_key() {
        let cache: Cache<String, Value> = Cache::builder().build();
        let immortal: Cache<String, Value> = Cache::builder().build();
        let app: AppState = AppState {
            cache: Arc::new(cache),
            immortal_cache: Arc::new(immortal),
            client: Client::new(),
            pg_registry: Arc::new(PostgresClientRegistry::empty()),
            gateway_force_camel_case_to_snake_case: false,
            pipeline_registry: None,
            logging_client_name: None,
        };
        let data: Data<AppState> = Data::new(app);
        let key: String = "key2".to_string();
        hydrate_cache_and_return_json(data.clone(), key.clone(), vec![json!({"v":1})]).await;
        hydrate_cache_and_return_json(data.clone(), key.clone(), vec![json!({"v":2})]).await;
        let cached: Value = data.cache.get(&key).await.unwrap();
        assert_eq!(cached, Value::Array(vec![json!({"v":2})]));
    }
}