Skip to main content

jpx_engine/
query_store.rs

1//! Session-scoped storage for named JMESPath queries.
2//!
3//! The query store allows you to save JMESPath expressions with names for
4//! reuse during a session. This is particularly useful for:
5//!
6//! - Building up complex queries iteratively
7//! - Reusing common extraction patterns
8//! - Organizing queries with descriptions
9//!
10//! # Example
11//!
12//! ```rust
13//! use jpx_engine::QueryStore;
14//! use jpx_engine::StoredQuery;
15//!
16//! let mut store = QueryStore::new();
17//!
18//! // Define a query
19//! store.define(StoredQuery {
20//!     name: "active_users".to_string(),
21//!     expression: "users[?active].name".to_string(),
22//!     description: Some("Get names of active users".to_string()),
23//! });
24//!
25//! // Retrieve it later
26//! let query = store.get("active_users").unwrap();
27//! assert_eq!(query.expression, "users[?active].name");
28//! ```
29//!
30//! # Thread Safety
31//!
32//! The [`QueryStore`] itself is not thread-safe. When used through
33//! [`JpxEngine`](crate::JpxEngine), it's wrapped in `Arc<RwLock<...>>`
34//! for safe concurrent access.
35
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38
39/// A named JMESPath query with optional description.
40///
41/// Stored queries can be defined, retrieved, and executed by name.
42///
43/// # Example
44///
45/// ```rust
46/// use jpx_engine::StoredQuery;
47///
48/// let query = StoredQuery {
49///     name: "count_items".to_string(),
50///     expression: "length(items)".to_string(),
51///     description: Some("Count the number of items".to_string()),
52/// };
53/// ```
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct StoredQuery {
56    /// Unique identifier for the query
57    pub name: String,
58    /// The JMESPath expression
59    pub expression: String,
60    /// Human-readable description of what the query does
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub description: Option<String>,
63}
64
65/// In-memory storage for named queries.
66///
67/// Provides CRUD operations for managing named JMESPath queries within a session.
68/// Queries are stored by name and can be listed alphabetically.
69///
70/// # Example
71///
72/// ```rust
73/// use jpx_engine::{QueryStore, StoredQuery};
74///
75/// let mut store = QueryStore::new();
76///
77/// // Add some queries
78/// store.define(StoredQuery {
79///     name: "first".to_string(),
80///     expression: "@[0]".to_string(),
81///     description: None,
82/// });
83///
84/// store.define(StoredQuery {
85///     name: "last".to_string(),
86///     expression: "@[-1]".to_string(),
87///     description: None,
88/// });
89///
90/// // List them (alphabetically sorted)
91/// let queries = store.list();
92/// assert_eq!(queries[0].name, "first");
93/// assert_eq!(queries[1].name, "last");
94///
95/// // Delete one
96/// store.delete("first");
97/// assert_eq!(store.len(), 1);
98/// ```
99#[derive(Debug, Default)]
100pub struct QueryStore {
101    queries: HashMap<String, StoredQuery>,
102}
103
104impl QueryStore {
105    /// Creates a new empty query store.
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Stores a named query.
111    ///
112    /// If a query with the same name already exists, it is replaced and
113    /// the old query is returned.
114    ///
115    /// # Returns
116    ///
117    /// `Some(StoredQuery)` if a query was replaced, `None` if this is a new name.
118    pub fn define(&mut self, query: StoredQuery) -> Option<StoredQuery> {
119        self.queries.insert(query.name.clone(), query)
120    }
121
122    /// Retrieves a query by name.
123    ///
124    /// # Returns
125    ///
126    /// `Some(&StoredQuery)` if found, `None` if no query has that name.
127    pub fn get(&self, name: &str) -> Option<&StoredQuery> {
128        self.queries.get(name)
129    }
130
131    /// Removes a query by name.
132    ///
133    /// # Returns
134    ///
135    /// `Some(StoredQuery)` containing the removed query, `None` if not found.
136    pub fn delete(&mut self, name: &str) -> Option<StoredQuery> {
137        self.queries.remove(name)
138    }
139
140    /// Lists all stored queries, sorted alphabetically by name.
141    pub fn list(&self) -> Vec<&StoredQuery> {
142        let mut queries: Vec<_> = self.queries.values().collect();
143        queries.sort_by(|a, b| a.name.cmp(&b.name));
144        queries
145    }
146
147    /// Returns the number of stored queries.
148    pub fn len(&self) -> usize {
149        self.queries.len()
150    }
151
152    /// Returns `true` if no queries are stored.
153    pub fn is_empty(&self) -> bool {
154        self.queries.is_empty()
155    }
156
157    /// Removes all stored queries.
158    pub fn clear(&mut self) {
159        self.queries.clear();
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_define_and_get() {
169        let mut store = QueryStore::new();
170
171        let query = StoredQuery {
172            name: "count".to_string(),
173            expression: "length(@)".to_string(),
174            description: Some("Count items".to_string()),
175        };
176
177        assert!(store.define(query.clone()).is_none());
178        assert_eq!(store.len(), 1);
179
180        let retrieved = store.get("count").unwrap();
181        assert_eq!(retrieved.name, "count");
182        assert_eq!(retrieved.expression, "length(@)");
183        assert_eq!(retrieved.description, Some("Count items".to_string()));
184    }
185
186    #[test]
187    fn test_define_overwrites() {
188        let mut store = QueryStore::new();
189
190        let query1 = StoredQuery {
191            name: "test".to_string(),
192            expression: "length(@)".to_string(),
193            description: None,
194        };
195
196        let query2 = StoredQuery {
197            name: "test".to_string(),
198            expression: "keys(@)".to_string(),
199            description: Some("Updated".to_string()),
200        };
201
202        assert!(store.define(query1).is_none());
203        let old = store.define(query2).unwrap();
204        assert_eq!(old.expression, "length(@)");
205
206        let current = store.get("test").unwrap();
207        assert_eq!(current.expression, "keys(@)");
208    }
209
210    #[test]
211    fn test_delete() {
212        let mut store = QueryStore::new();
213
214        let query = StoredQuery {
215            name: "to_delete".to_string(),
216            expression: "`null`".to_string(),
217            description: None,
218        };
219
220        store.define(query);
221        assert_eq!(store.len(), 1);
222
223        let deleted = store.delete("to_delete").unwrap();
224        assert_eq!(deleted.name, "to_delete");
225        assert_eq!(store.len(), 0);
226
227        assert!(store.delete("nonexistent").is_none());
228    }
229
230    #[test]
231    fn test_list() {
232        let mut store = QueryStore::new();
233
234        store.define(StoredQuery {
235            name: "zebra".to_string(),
236            expression: "`1`".to_string(),
237            description: None,
238        });
239        store.define(StoredQuery {
240            name: "alpha".to_string(),
241            expression: "`2`".to_string(),
242            description: None,
243        });
244        store.define(StoredQuery {
245            name: "beta".to_string(),
246            expression: "`3`".to_string(),
247            description: None,
248        });
249
250        let list = store.list();
251        assert_eq!(list.len(), 3);
252        // Should be sorted alphabetically
253        assert_eq!(list[0].name, "alpha");
254        assert_eq!(list[1].name, "beta");
255        assert_eq!(list[2].name, "zebra");
256    }
257
258    #[test]
259    fn test_clear() {
260        let mut store = QueryStore::new();
261
262        store.define(StoredQuery {
263            name: "a".to_string(),
264            expression: "`1`".to_string(),
265            description: None,
266        });
267        store.define(StoredQuery {
268            name: "b".to_string(),
269            expression: "`2`".to_string(),
270            description: None,
271        });
272
273        assert_eq!(store.len(), 2);
274        store.clear();
275        assert!(store.is_empty());
276    }
277}