coman/core/
collection_manager.rs

1//! Collection Manager - Core business logic for managing API collections
2//!
3//! This module provides a clean API for managing collections and endpoints
4//! without any CLI dependencies.
5
6use std::sync::Arc;
7
8use tokio::sync::Mutex;
9
10use crate::core::errors::CollectionError;
11use crate::models::collection::Collection;
12use crate::{helper, Request};
13
14/// Result type for collection operations
15pub type CollectionResult<T> = Result<T, CollectionError>;
16
17/// Manager for API collections
18///
19/// Provides methods for CRUD operations on collections and endpoints.
20#[derive(Clone)]
21pub struct CollectionManager {
22    in_memory: bool,
23    file_path: Option<String>,
24    loaded_collections: Arc<Mutex<Vec<Collection>>>,
25}
26
27impl Default for CollectionManager {
28    fn default() -> Self {
29        Self::new(None, false)
30    }
31}
32
33impl CollectionManager {
34    /// Create a new CollectionManager
35    ///
36    /// # Arguments
37    ///
38    /// * `file_path` - Optional custom file path. If None, uses default location.
39    pub fn new(file_path: Option<String>, in_memory: bool) -> Self {
40        if let Some(ref path) = file_path {
41            std::env::set_var("COMAN_JSON", path);
42        }
43        Self {
44            in_memory,
45            file_path,
46            loaded_collections: if in_memory {
47                Arc::new(Mutex::new(Vec::new()))
48            } else {
49                Self::load_collections_from_file()
50                    .unwrap_or_else(|_| Arc::new(Mutex::new(Vec::new())))
51            },
52        }
53    }
54
55    /// Get the file path being used
56    pub fn get_file_path(&self) -> String {
57        self.file_path
58            .clone()
59            .unwrap_or_else(|| helper::get_file_path().to_string())
60    }
61
62    /// Load all collections from the storage file
63    fn load_collections_from_file() -> CollectionResult<Arc<Mutex<Vec<Collection>>>> {
64        match helper::read_json_from_file::<Vec<Collection>>() {
65            Ok(c) => Ok(Arc::new(Mutex::new(c))),
66            Err(e) => {
67                if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
68                    if io_err.kind() == std::io::ErrorKind::NotFound {
69                        Ok(Arc::new(Mutex::new(Vec::new())))
70                    } else {
71                        Err(CollectionError::Other(e.to_string()))
72                    }
73                } else {
74                    Err(CollectionError::Other(e.to_string()))
75                }
76            }
77        }
78    }
79
80    /// Get loaded collections
81    pub async fn get_collections(&self) -> Vec<Collection> {
82        let collections = self.loaded_collections.lock().await;
83        collections.clone()
84    }
85
86    /// Get a specific Collection by name
87    pub async fn get_collection(&self, name: &str) -> CollectionResult<Option<Collection>> {
88        let collections = self.loaded_collections.lock().await;
89        for c in collections.iter() {
90            if c.name == name {
91                return Ok(Some(c.clone()));
92            }
93        }
94        Err(CollectionError::CollectionNotFound(name.to_string()))
95    }
96
97    /// Get a specific Endpoint by collection name and request name
98    pub async fn get_endpoint(
99        &self,
100        col_name: &str,
101        ep_name: &str,
102    ) -> CollectionResult<Option<Request>> {
103        let collections = self.loaded_collections.lock().await;
104        for c in collections.iter() {
105            if c.name == col_name {
106                if let Some(ref requests) = c.requests {
107                    for r in requests.iter() {
108                        if r.name == ep_name {
109                            return Ok(Some(r.clone()));
110                        }
111                    }
112                }
113            }
114        }
115        Err(CollectionError::EndpointNotFound(format!(
116            "{} in {}",
117            ep_name, col_name
118        )))
119    }
120
121    /// Update an existing collection or add a new one
122    pub async fn update_add_collection(&self, updated: Collection) -> CollectionResult<()> {
123        let mut collections = self.loaded_collections.lock().await;
124        if let Some(pos) = collections.iter().position(|c| c.name == updated.name) {
125            collections[pos] = updated;
126            if !self.in_memory {
127                helper::write_json_to_file(&*collections)?;
128            }
129            Ok(())
130        } else {
131            collections.push(updated);
132            if !self.in_memory {
133                helper::write_json_to_file(&*collections)?;
134            }
135            Ok(())
136        }
137    }
138
139    /// Update an existing request within a collection or add a new one
140    pub async fn update_add_request(
141        &self,
142        col_name: &str,
143        ep_name: &str,
144        updated: Request,
145    ) -> CollectionResult<()> {
146        let mut collections = self.loaded_collections.lock().await;
147        if let Some(col_pos) = collections.iter().position(|c| c.name == col_name) {
148            if let Some(ref mut requests) = collections[col_pos].requests {
149                if let Some(req_pos) = requests.iter().position(|r| r.name == ep_name) {
150                    requests[req_pos] = updated;
151                    if !self.in_memory {
152                        helper::write_json_to_file(&*collections)?;
153                    }
154                    return Ok(());
155                } else {
156                    requests.push(updated);
157                    if !self.in_memory {
158                        helper::write_json_to_file(&*collections)?;
159                    }
160                    return Ok(());
161                }
162            } else {
163                collections[col_pos].requests = Some(vec![updated]);
164                if !self.in_memory {
165                    helper::write_json_to_file(&*collections)?;
166                }
167                return Ok(());
168            }
169        }
170        Err(CollectionError::CollectionNotFound(col_name.to_string()))
171    }
172
173    /// Delete a collection
174    pub async fn delete_collection(&self, name: &str) -> CollectionResult<()> {
175        match self.get_collection(name).await {
176            Ok(_) => {
177                let mut collections = self.loaded_collections.lock().await;
178                collections.retain(|c| c.name != name);
179                if !self.in_memory {
180                    helper::write_json_to_file(&*collections)?;
181                }
182                Ok(())
183            }
184            Err(_) => Err(CollectionError::CollectionNotFound(name.to_string())),
185        }
186    }
187
188    /// Save collections to the storage file
189    pub async fn save_loaded_collections(self) -> CollectionResult<()> {
190        if !self.in_memory {
191            let collections = self.loaded_collections.lock().await;
192            helper::write_json_to_file(&*collections)?;
193        }
194        Ok(())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200
201    use super::*;
202    use serial_test::serial;
203
204    fn setup_test_manager() -> CollectionManager {
205        std::env::set_var("COMAN_JSON", "test.json");
206        CollectionManager::new(Some("test.json".to_string()), false)
207    }
208
209    #[tokio::test]
210    #[serial]
211    async fn test_load_collections() {
212        let manager = setup_test_manager();
213        let result = manager.get_collections().await;
214        assert!(!result.is_empty());
215    }
216
217    #[tokio::test]
218    #[serial]
219    async fn test_get_collection() {
220        let manager = setup_test_manager();
221
222        let result = manager.get_collection("coman").await;
223
224        assert!(result.is_ok());
225    }
226
227    #[tokio::test]
228    #[serial]
229    async fn test_save_collections() {
230        let manager = setup_test_manager();
231
232        let result = manager.save_loaded_collections().await;
233        assert!(result.is_ok());
234    }
235}