ppoppo-infra 0.1.0

Backend-agnostic infrastructure traits for caching, queuing, and messaging
Documentation
//! Extension traits with typed convenience methods.
//!
//! These traits provide `Serialize`/`DeserializeOwned` wrappers around
//! the `serde_json::Value`-based core traits. They are automatically
//! available for any type implementing the core trait via blanket impls.

use async_trait::async_trait;
use serde::{Serialize, de::DeserializeOwned};

use crate::Result;
use crate::cache::Cache;
use crate::job_queue::JobQueue;
use crate::publisher::Publisher;
use crate::types::Priority;

/// Typed convenience methods for [`Cache`].
#[async_trait]
pub trait CacheExt: Cache {
    /// Set a typed value with optional TTL in seconds.
    async fn set_typed<T: Serialize + Send + Sync>(
        &self,
        key: &str,
        value: &T,
        ttl_seconds: Option<i32>,
    ) -> Result<()> {
        let json = serde_json::to_value(value)?;
        self.set(key, &json, ttl_seconds).await
    }

    /// Get a typed value if it exists and is not expired.
    async fn get_typed<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
        match self.get(key).await? {
            Some(value) => Ok(Some(serde_json::from_value(value)?)),
            None => Ok(None),
        }
    }

    /// Get multiple typed values atomically.
    async fn mget_typed<T: DeserializeOwned>(
        &self,
        keys: &[&str],
    ) -> Result<Vec<(String, Option<T>)>> {
        let raw = self.mget(keys).await?;
        let mut results = Vec::with_capacity(raw.len());
        for (key, value) in raw {
            let parsed = match value {
                Some(v) => Some(serde_json::from_value(v)?),
                None => None,
            };
            results.push((key, parsed));
        }
        Ok(results)
    }
}

/// Blanket impl: any Cache automatically gets CacheExt.
impl<T: Cache + ?Sized> CacheExt for T {}

/// Typed convenience methods for [`JobQueue`].
#[async_trait]
pub trait JobQueueExt: JobQueue {
    /// Push a typed payload to the queue. Returns the generated job ID.
    async fn push_typed<T: Serialize + Send + Sync>(
        &self,
        queue_name: &str,
        payload: &T,
        priority: Priority,
    ) -> Result<String> {
        let json = serde_json::to_value(payload)?;
        self.push(queue_name, &json, priority).await
    }

    /// Push a typed payload with full options.
    async fn push_typed_with_options<T: Serialize + Send + Sync>(
        &self,
        queue_name: &str,
        payload: &T,
        priority: Priority,
        delay_seconds: i32,
        max_attempts: i32,
    ) -> Result<String> {
        let json = serde_json::to_value(payload)?;
        self.push_with_options(queue_name, &json, priority, delay_seconds, max_attempts)
            .await
    }

    /// Pop and deserialize a typed job from the queue.
    async fn pop_typed<T: DeserializeOwned>(
        &self,
        queue_name: &str,
        lock_duration_seconds: i32,
    ) -> Result<Option<crate::types::TypedJob<T>>> {
        match self.pop(queue_name, lock_duration_seconds).await? {
            Some(job) => {
                let payload: T = serde_json::from_value(job.payload)?;
                Ok(Some(crate::types::TypedJob {
                    id: job.id,
                    queue_name: job.queue_name,
                    payload,
                    attempts: job.attempts,
                    max_attempts: job.max_attempts,
                    created_at: job.created_at,
                }))
            }
            None => Ok(None),
        }
    }
}

/// Blanket impl: any JobQueue automatically gets JobQueueExt.
impl<T: JobQueue + ?Sized> JobQueueExt for T {}

/// Typed convenience methods for [`Publisher`].
#[async_trait]
pub trait PublisherExt: Publisher {
    /// Publish a typed payload to a channel.
    async fn publish_typed<T: Serialize + Send + Sync>(
        &self,
        channel: &str,
        payload: &T,
    ) -> Result<()> {
        let json = serde_json::to_value(payload)?;
        self.publish(channel, &json).await
    }

    /// Publish a typed payload to multiple channels atomically.
    async fn publish_multi_typed<T: Serialize + Send + Sync>(
        &self,
        channels: &[&str],
        payload: &T,
    ) -> Result<()> {
        let json = serde_json::to_value(payload)?;
        self.publish_multi(channels, &json).await
    }
}

/// Blanket impl: any Publisher automatically gets PublisherExt.
impl<T: Publisher + ?Sized> PublisherExt for T {}