Skip to main content

backbone_core/
query.rs

1//! CQRS Query Pattern
2//!
3//! Provides traits for implementing the Query side of CQRS.
4//! Queries represent requests for information without side effects.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use backbone_core::{Query, QueryHandler};
10//!
11//! // Define a query
12//! pub struct GetUserByEmailQuery {
13//!     pub email: String,
14//! }
15//!
16//! impl Query for GetUserByEmailQuery {
17//!     type Result = Option<UserDto>;
18//! }
19//!
20//! // Implement the handler
21//! pub struct GetUserByEmailHandler {
22//!     read_model: Arc<dyn UserReadModel>,
23//! }
24//!
25//! #[async_trait::async_trait]
26//! impl QueryHandler<GetUserByEmailQuery> for GetUserByEmailHandler {
27//!     type Error = QueryError;
28//!
29//!     async fn handle(&self, query: GetUserByEmailQuery) -> Result<Option<UserDto>, Self::Error> {
30//!         self.read_model.find_by_email(&query.email).await
31//!     }
32//! }
33//! ```
34
35use async_trait::async_trait;
36
37/// Marker trait for CQRS queries.
38///
39/// Queries represent requests for information and should not
40/// cause any side effects in the system.
41pub trait Query: Send + Sync {
42    /// The result type returned by the query.
43    type Result: Send + Sync;
44}
45
46/// Handler for executing queries.
47///
48/// Query handlers retrieve data from read models and should
49/// be optimized for read performance.
50#[async_trait]
51pub trait QueryHandler<Q: Query>: Send + Sync {
52    /// Error type for query execution failures.
53    type Error: std::error::Error + Send + Sync;
54
55    /// Execute the query and return the result.
56    async fn handle(&self, query: Q) -> Result<Q::Result, Self::Error>;
57}
58
59/// Query dispatcher for routing queries to their handlers.
60///
61/// Provides a central point for query execution with
62/// optional caching and middleware support.
63#[async_trait]
64pub trait QueryDispatcher: Send + Sync {
65    /// Dispatch a query to its handler.
66    async fn dispatch<Q: Query>(
67        &self,
68        query: Q,
69    ) -> Result<Q::Result, Box<dyn std::error::Error + Send + Sync>>;
70}
71
72/// Trait for queries that support caching.
73pub trait CacheableQuery: Query {
74    /// Cache key for this query.
75    fn cache_key(&self) -> String;
76
77    /// Time-to-live for cached results in seconds.
78    fn cache_ttl(&self) -> Option<u64> {
79        None // No caching by default
80    }
81}
82
83/// Trait for paginated queries.
84pub trait PaginatedQuery: Query {
85    /// Get the page number (1-based).
86    fn page(&self) -> u32;
87
88    /// Get the page size.
89    fn page_size(&self) -> u32;
90
91    /// Get the offset for database queries.
92    fn offset(&self) -> u32 {
93        (self.page().saturating_sub(1)) * self.page_size()
94    }
95}
96
97/// Result wrapper for paginated queries.
98#[derive(Debug, Clone)]
99pub struct PaginatedQueryResult<T> {
100    /// The items for the current page.
101    pub items: Vec<T>,
102    /// Total number of items across all pages.
103    pub total: u64,
104    /// Current page number (1-based).
105    pub page: u32,
106    /// Number of items per page.
107    pub page_size: u32,
108    /// Total number of pages.
109    pub total_pages: u32,
110}
111
112impl<T> PaginatedQueryResult<T> {
113    /// Create a new paginated result.
114    pub fn new(items: Vec<T>, total: u64, page: u32, page_size: u32) -> Self {
115        let total_pages = if page_size > 0 {
116            ((total as f64) / (page_size as f64)).ceil() as u32
117        } else {
118            0
119        };
120
121        Self {
122            items,
123            total,
124            page,
125            page_size,
126            total_pages,
127        }
128    }
129
130    /// Check if there's a next page.
131    pub fn has_next(&self) -> bool {
132        self.page < self.total_pages
133    }
134
135    /// Check if there's a previous page.
136    pub fn has_previous(&self) -> bool {
137        self.page > 1
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    struct TestQuery {
146        id: String,
147    }
148
149    impl Query for TestQuery {
150        type Result = String;
151    }
152
153    struct TestHandler;
154
155    #[async_trait]
156    impl QueryHandler<TestQuery> for TestHandler {
157        type Error = std::io::Error;
158
159        async fn handle(&self, query: TestQuery) -> Result<String, Self::Error> {
160            Ok(format!("Result for {}", query.id))
161        }
162    }
163
164    #[tokio::test]
165    async fn test_query_handler() {
166        let handler = TestHandler;
167        let query = TestQuery {
168            id: "123".to_string(),
169        };
170        let result = handler.handle(query).await.unwrap();
171        assert_eq!(result, "Result for 123");
172    }
173
174    #[test]
175    fn test_paginated_result() {
176        let result: PaginatedQueryResult<i32> = PaginatedQueryResult::new(vec![1, 2, 3], 10, 1, 3);
177
178        assert_eq!(result.total_pages, 4);
179        assert!(result.has_next());
180        assert!(!result.has_previous());
181    }
182}