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::collections::HashMap;
7
8use crate::helper;
9use crate::models::collection::{Collection, Method, Request};
10
11/// Result type for collection operations
12pub type CollectionResult<T> = Result<T, CollectionError>;
13
14/// Errors that can occur during collection operations
15#[derive(Debug)]
16pub enum CollectionError {
17    /// Collection was not found
18    CollectionNotFound(String),
19    /// Endpoint was not found
20    EndpointNotFound(String),
21    /// IO error occurred
22    IoError(std::io::Error),
23    /// JSON serialization/deserialization error
24    JsonError(serde_json::Error),
25    /// Generic error with message
26    Other(String),
27}
28
29impl std::fmt::Display for CollectionError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            CollectionError::CollectionNotFound(name) => {
33                write!(f, "Collection not found: {}", name)
34            }
35            CollectionError::EndpointNotFound(name) => write!(f, "Endpoint not found: {}", name),
36            CollectionError::IoError(e) => write!(f, "IO error: {}", e),
37            CollectionError::JsonError(e) => write!(f, "JSON error: {}", e),
38            CollectionError::Other(msg) => write!(f, "{}", msg),
39        }
40    }
41}
42
43impl std::error::Error for CollectionError {}
44
45impl From<std::io::Error> for CollectionError {
46    fn from(err: std::io::Error) -> Self {
47        CollectionError::IoError(err)
48    }
49}
50
51impl From<serde_json::Error> for CollectionError {
52    fn from(err: serde_json::Error) -> Self {
53        CollectionError::JsonError(err)
54    }
55}
56
57impl From<Box<dyn std::error::Error>> for CollectionError {
58    fn from(err: Box<dyn std::error::Error>) -> Self {
59        CollectionError::Other(err.to_string())
60    }
61}
62
63/// Manager for API collections
64///
65/// Provides methods for CRUD operations on collections and endpoints.
66#[derive(Clone)]
67pub struct CollectionManager {
68    file_path: Option<String>,
69}
70
71impl Default for CollectionManager {
72    fn default() -> Self {
73        Self::new(None)
74    }
75}
76
77impl CollectionManager {
78    /// Create a new CollectionManager
79    ///
80    /// # Arguments
81    ///
82    /// * `file_path` - Optional custom file path. If None, uses default location.
83    pub fn new(file_path: Option<String>) -> Self {
84        if let Some(ref path) = file_path {
85            std::env::set_var("COMAN_JSON", path);
86        }
87        Self { file_path }
88    }
89
90    /// Get the file path being used
91    pub fn get_file_path(&self) -> String {
92        self.file_path
93            .clone()
94            .unwrap_or_else(|| helper::get_file_path().to_string())
95    }
96
97    /// Load all collections from the storage file
98    pub fn load_collections(&self) -> CollectionResult<Vec<Collection>> {
99        match helper::read_json_from_file() {
100            Ok(c) => Ok(c),
101            Err(e) => {
102                if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
103                    if io_err.kind() == std::io::ErrorKind::NotFound {
104                        Ok(Vec::new())
105                    } else {
106                        Err(CollectionError::Other(e.to_string()))
107                    }
108                } else {
109                    Err(CollectionError::Other(e.to_string()))
110                }
111            }
112        }
113    }
114
115    /// Save collections to the storage file
116    pub fn save_collections(&self, collections: &[Collection]) -> CollectionResult<()> {
117        let vec: Vec<Collection> = collections.to_vec();
118        helper::write_json_to_file(&vec).map_err(|e| CollectionError::Other(e.to_string()))
119    }
120
121    /// Get a specific collection by name
122    pub fn get_collection(&self, name: &str) -> CollectionResult<Collection> {
123        let collections = self.load_collections()?;
124        collections
125            .into_iter()
126            .find(|c| c.name == name)
127            .ok_or_else(|| CollectionError::CollectionNotFound(name.to_string()))
128    }
129
130    /// Get a specific endpoint from a collection
131    pub fn get_endpoint(&self, collection: &str, endpoint: &str) -> CollectionResult<Request> {
132        let col = self.get_collection(collection)?;
133        col.requests
134            .and_then(|requests| requests.into_iter().find(|r| r.name == endpoint))
135            .ok_or_else(|| CollectionError::EndpointNotFound(endpoint.to_string()))
136    }
137
138    /// Get the full URL for an endpoint (base URL + endpoint path)
139    pub fn get_endpoint_url(&self, collection: &str, endpoint: &str) -> CollectionResult<String> {
140        let col = self.get_collection(collection)?;
141        let req = self.get_endpoint(collection, endpoint)?;
142        Ok(format!("{}{}", col.url, req.endpoint))
143    }
144
145    /// Get merged headers for an endpoint (collection headers + endpoint headers)
146    pub fn get_endpoint_headers(
147        &self,
148        collection: &str,
149        endpoint: &str,
150    ) -> CollectionResult<Vec<(String, String)>> {
151        let col = self.get_collection(collection)?;
152        let req = self.get_endpoint(collection, endpoint)?;
153
154        let mut merged: HashMap<String, String> = HashMap::new();
155        for (k, v) in &col.headers {
156            merged.insert(k.clone(), v.clone());
157        }
158        for (k, v) in &req.headers {
159            merged.insert(k.clone(), v.clone());
160        }
161
162        Ok(merged.into_iter().collect())
163    }
164
165    /// Add a new collection
166    ///
167    /// If a collection with the same name exists, it will be updated.
168    pub fn add_collection(
169        &self,
170        name: &str,
171        url: &str,
172        headers: Vec<(String, String)>,
173    ) -> CollectionResult<()> {
174        let mut collections = self.load_collections()?;
175
176        if let Some(col) = collections.iter_mut().find(|c| c.name == name) {
177            // Update existing collection
178            col.url = url.to_string();
179            col.headers = headers;
180        } else {
181            // Add new collection
182            collections.push(Collection {
183                name: name.to_string(),
184                url: url.to_string(),
185                headers,
186                requests: None,
187            });
188        }
189
190        self.save_collections(&collections)
191    }
192
193    /// Delete a collection
194    pub fn delete_collection(&self, name: &str) -> CollectionResult<()> {
195        let mut collections = self.load_collections()?;
196        let original_len = collections.len();
197        collections.retain(|c| c.name != name);
198
199        if collections.len() == original_len {
200            return Err(CollectionError::CollectionNotFound(name.to_string()));
201        }
202
203        self.save_collections(&collections)
204    }
205
206    /// Update a collection
207    pub fn update_collection(
208        &self,
209        name: &str,
210        url: Option<&str>,
211        headers: Option<Vec<(String, String)>>,
212    ) -> CollectionResult<()> {
213        let mut collections = self.load_collections()?;
214
215        let col = collections
216            .iter_mut()
217            .find(|c| c.name == name)
218            .ok_or_else(|| CollectionError::CollectionNotFound(name.to_string()))?;
219
220        if let Some(url) = url {
221            col.url = url.to_string();
222        }
223
224        if let Some(new_headers) = headers {
225            col.headers = Self::merge_headers(col.headers.clone(), &new_headers);
226        }
227
228        self.save_collections(&collections)
229    }
230
231    /// Copy a collection to a new name
232    pub fn copy_collection(&self, name: &str, new_name: &str) -> CollectionResult<()> {
233        let mut collections = self.load_collections()?;
234
235        let col = collections
236            .iter()
237            .find(|c| c.name == name)
238            .ok_or_else(|| CollectionError::CollectionNotFound(name.to_string()))?;
239
240        let mut new_col = col.clone();
241        new_col.name = new_name.to_string();
242        collections.push(new_col);
243
244        self.save_collections(&collections)
245    }
246
247    /// Add an endpoint to a collection
248    ///
249    /// If an endpoint with the same name exists, it will be updated.
250    pub fn add_endpoint(
251        &self,
252        collection: &str,
253        name: &str,
254        path: &str,
255        method: Method,
256        headers: Vec<(String, String)>,
257        body: Option<String>,
258    ) -> CollectionResult<()> {
259        let mut collections = self.load_collections()?;
260
261        let col = collections
262            .iter_mut()
263            .find(|c| c.name == collection)
264            .ok_or_else(|| CollectionError::CollectionNotFound(collection.to_string()))?;
265
266        let request = Request {
267            name: name.to_string(),
268            endpoint: path.to_string(),
269            method,
270            headers,
271            body,
272        };
273
274        let mut requests = col.requests.clone().unwrap_or_default();
275        requests.retain(|r| r.name != name);
276        requests.push(request);
277        col.requests = Some(requests);
278
279        self.save_collections(&collections)
280    }
281
282    /// Delete an endpoint from a collection
283    pub fn delete_endpoint(&self, collection: &str, endpoint: &str) -> CollectionResult<()> {
284        let mut collections = self.load_collections()?;
285
286        let col = collections
287            .iter_mut()
288            .find(|c| c.name == collection)
289            .ok_or_else(|| CollectionError::CollectionNotFound(collection.to_string()))?;
290
291        if let Some(requests) = col.requests.as_mut() {
292            let original_len = requests.len();
293            requests.retain(|r| r.name != endpoint);
294
295            if requests.len() == original_len {
296                return Err(CollectionError::EndpointNotFound(endpoint.to_string()));
297            }
298        } else {
299            return Err(CollectionError::EndpointNotFound(endpoint.to_string()));
300        }
301
302        self.save_collections(&collections)
303    }
304
305    /// Update an endpoint in a collection
306    pub fn update_endpoint(
307        &self,
308        collection: &str,
309        endpoint: &str,
310        path: Option<&str>,
311        headers: Option<Vec<(String, String)>>,
312        body: Option<String>,
313    ) -> CollectionResult<()> {
314        let mut collections = self.load_collections()?;
315
316        let col = collections
317            .iter_mut()
318            .find(|c| c.name == collection)
319            .ok_or_else(|| CollectionError::CollectionNotFound(collection.to_string()))?;
320
321        if let Some(requests) = col.requests.as_mut() {
322            let req = requests
323                .iter_mut()
324                .find(|r| r.name == endpoint)
325                .ok_or_else(|| CollectionError::EndpointNotFound(endpoint.to_string()))?;
326
327            if let Some(path) = path {
328                req.endpoint = path.to_string();
329            }
330
331            if let Some(new_headers) = headers {
332                req.headers = Self::merge_headers(req.headers.clone(), &new_headers);
333            }
334
335            if let Some(new_body) = body {
336                req.body = if new_body.is_empty() {
337                    None
338                } else {
339                    Some(new_body)
340                };
341            }
342        } else {
343            return Err(CollectionError::EndpointNotFound(endpoint.to_string()));
344        }
345
346        self.save_collections(&collections)
347    }
348
349    /// Copy an endpoint within the same collection or to another collection
350    pub fn copy_endpoint(
351        &self,
352        collection: &str,
353        endpoint: &str,
354        new_name: &str,
355        to_collection: Option<&str>,
356    ) -> CollectionResult<()> {
357        let mut collections = self.load_collections()?;
358
359        // Find the source endpoint
360        let source_col = collections
361            .iter()
362            .find(|c| c.name == collection)
363            .ok_or_else(|| CollectionError::CollectionNotFound(collection.to_string()))?;
364
365        let source_req = source_col
366            .requests
367            .as_ref()
368            .and_then(|r| r.iter().find(|r| r.name == endpoint))
369            .ok_or_else(|| CollectionError::EndpointNotFound(endpoint.to_string()))?;
370
371        let mut new_req = source_req.clone();
372
373        if let Some(target_col_name) = to_collection {
374            // Copy to another collection (keep original name)
375            let target_col = collections
376                .iter_mut()
377                .find(|c| c.name == target_col_name)
378                .ok_or_else(|| CollectionError::CollectionNotFound(target_col_name.to_string()))?;
379
380            let mut requests = target_col.requests.clone().unwrap_or_default();
381            requests.push(new_req);
382            target_col.requests = Some(requests);
383        } else {
384            // Copy within the same collection with a new name
385            new_req.name = new_name.to_string();
386
387            let col = collections
388                .iter_mut()
389                .find(|c| c.name == collection)
390                .ok_or_else(|| CollectionError::CollectionNotFound(collection.to_string()))?;
391
392            let mut requests = col.requests.clone().unwrap_or_default();
393            requests.push(new_req);
394            col.requests = Some(requests);
395        }
396
397        self.save_collections(&collections)
398    }
399
400    /// List all collections
401    pub fn list_collections(&self) -> CollectionResult<Vec<Collection>> {
402        self.load_collections()
403    }
404
405    /// Merge headers, replacing existing ones and removing those with empty values
406    fn merge_headers(
407        existing: Vec<(String, String)>,
408        new_headers: &[(String, String)],
409    ) -> Vec<(String, String)> {
410        let mut merged: HashMap<String, String> = existing.into_iter().collect();
411        for (key, value) in new_headers.iter() {
412            if merged.contains_key(key) {
413                if value.is_empty() {
414                    merged.remove(key);
415                } else {
416                    merged.entry(key.clone()).and_modify(|v| *v = value.clone());
417                }
418            } else {
419                merged.insert(key.clone(), value.clone());
420            }
421        }
422        merged.into_iter().collect()
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use serial_test::serial;
430
431    fn setup_test_manager() -> CollectionManager {
432        std::env::set_var("COMAN_JSON", "test.json");
433        CollectionManager::new(Some("test.json".to_string()))
434    }
435
436    #[test]
437    #[serial]
438    fn test_load_collections() {
439        let manager = setup_test_manager();
440        let result = manager.load_collections();
441        assert!(result.is_ok());
442    }
443
444    #[test]
445    #[serial]
446    fn test_get_collection() {
447        let manager = setup_test_manager();
448        // This test assumes there's a collection in test.json
449        let result = manager.load_collections();
450        assert!(result.is_ok());
451    }
452}