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}