metabase_api_rs/repository/
collection.rs

1//! Collection repository trait and implementations
2//!
3//! This module provides the repository abstraction for Collection entities.
4
5use super::traits::{
6    FilterParams, PaginationParams, Repository, RepositoryError, RepositoryResult,
7};
8use crate::core::models::common::CollectionId;
9use crate::core::models::Collection;
10use crate::transport::http_provider_safe::{HttpProviderExt, HttpProviderSafe};
11use async_trait::async_trait;
12use std::sync::Arc;
13
14/// Collection-specific filter parameters
15#[derive(Debug, Clone, Default)]
16pub struct CollectionFilterParams {
17    /// Base filters
18    pub base: FilterParams,
19    /// Filter by parent collection ID
20    pub parent_id: Option<i32>,
21    /// Filter by namespace (e.g., "snippets", "cards")
22    pub namespace: Option<String>,
23    /// Filter by personal collection
24    pub personal_only: Option<bool>,
25}
26
27impl CollectionFilterParams {
28    /// Create new collection filter params
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set parent ID filter
34    pub fn with_parent(mut self, parent_id: i32) -> Self {
35        self.parent_id = Some(parent_id);
36        self
37    }
38
39    /// Set namespace filter
40    pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
41        self.namespace = Some(namespace.into());
42        self
43    }
44
45    /// Set personal collection filter
46    pub fn with_personal_only(mut self, personal_only: bool) -> Self {
47        self.personal_only = Some(personal_only);
48        self
49    }
50}
51
52/// Repository trait for Collection entities
53#[async_trait]
54pub trait CollectionRepository:
55    Repository<Entity = Collection, Id = CollectionId> + Send + Sync
56{
57    /// List collections with collection-specific filters
58    async fn list_with_filters(
59        &self,
60        pagination: Option<PaginationParams>,
61        filters: Option<CollectionFilterParams>,
62    ) -> RepositoryResult<Vec<Collection>>;
63
64    /// Get child collections of a specific collection
65    async fn get_children(&self, parent_id: CollectionId) -> RepositoryResult<Vec<Collection>>;
66
67    /// Get root collections
68    async fn get_root_collections(&self) -> RepositoryResult<Vec<Collection>>;
69
70    /// Get collections by parent ID
71    async fn get_by_parent(
72        &self,
73        parent_id: Option<CollectionId>,
74    ) -> RepositoryResult<Vec<Collection>>;
75
76    /// Get collection permissions
77    async fn get_permissions(&self, id: &CollectionId) -> RepositoryResult<serde_json::Value>;
78
79    /// Update collection permissions
80    async fn update_permissions(
81        &self,
82        id: &CollectionId,
83        permissions: &serde_json::Value,
84    ) -> RepositoryResult<()>;
85
86    /// Move collection to another parent
87    async fn move_collection(
88        &self,
89        id: &CollectionId,
90        new_parent_id: Option<CollectionId>,
91    ) -> RepositoryResult<Collection>;
92
93    /// Archive a collection
94    async fn archive(&self, id: &CollectionId) -> RepositoryResult<()>;
95
96    /// Unarchive a collection
97    async fn unarchive(&self, id: &CollectionId) -> RepositoryResult<()>;
98}
99
100/// HTTP implementation of CollectionRepository
101pub struct HttpCollectionRepository {
102    http_provider: Arc<dyn HttpProviderSafe>,
103}
104
105impl HttpCollectionRepository {
106    /// Create a new HTTP collection repository
107    pub fn new(http_provider: Arc<dyn HttpProviderSafe>) -> Self {
108        Self { http_provider }
109    }
110
111    /// Convert filter params to query string
112    fn build_query_params(
113        &self,
114        pagination: Option<PaginationParams>,
115        filters: Option<FilterParams>,
116    ) -> String {
117        let mut params = Vec::new();
118
119        if let Some(p) = pagination {
120            if let Some(page) = p.page {
121                params.push(format!("page={}", page));
122            }
123            if let Some(limit) = p.limit {
124                params.push(format!("limit={}", limit));
125            }
126            if let Some(offset) = p.offset {
127                params.push(format!("offset={}", offset));
128            }
129        }
130
131        if let Some(f) = filters {
132            if let Some(query) = f.query {
133                params.push(format!("q={}", query.replace(' ', "+")));
134            }
135            if let Some(archived) = f.archived {
136                params.push(format!("archived={}", archived));
137            }
138        }
139
140        if params.is_empty() {
141            String::new()
142        } else {
143            format!("?{}", params.join("&"))
144        }
145    }
146}
147
148#[async_trait]
149impl Repository for HttpCollectionRepository {
150    type Entity = Collection;
151    type Id = CollectionId;
152
153    async fn get(&self, id: &CollectionId) -> RepositoryResult<Collection> {
154        let path = format!("/api/collection/{}", id.0);
155        self.http_provider.get(&path).await.map_err(|e| e.into())
156    }
157
158    async fn list(
159        &self,
160        pagination: Option<PaginationParams>,
161        filters: Option<FilterParams>,
162    ) -> RepositoryResult<Vec<Collection>> {
163        let query = self.build_query_params(pagination, filters);
164        let path = format!("/api/collection{}", query);
165        self.http_provider.get(&path).await.map_err(|e| e.into())
166    }
167
168    async fn create(&self, entity: &Collection) -> RepositoryResult<Collection> {
169        self.http_provider
170            .post("/api/collection", entity)
171            .await
172            .map_err(|e| e.into())
173    }
174
175    async fn update(&self, id: &CollectionId, entity: &Collection) -> RepositoryResult<Collection> {
176        let path = format!("/api/collection/{}", id.0);
177        self.http_provider
178            .put(&path, entity)
179            .await
180            .map_err(|e| e.into())
181    }
182
183    async fn delete(&self, id: &CollectionId) -> RepositoryResult<()> {
184        let path = format!("/api/collection/{}", id.0);
185        self.http_provider.delete(&path).await.map_err(|e| e.into())
186    }
187}
188
189#[async_trait]
190impl CollectionRepository for HttpCollectionRepository {
191    async fn list_with_filters(
192        &self,
193        pagination: Option<PaginationParams>,
194        filters: Option<CollectionFilterParams>,
195    ) -> RepositoryResult<Vec<Collection>> {
196        // Convert CollectionFilterParams to FilterParams
197        let base_filters = filters.map(|f| f.base);
198        self.list(pagination, base_filters).await
199    }
200
201    async fn get_children(&self, parent_id: CollectionId) -> RepositoryResult<Vec<Collection>> {
202        let path = format!("/api/collection/{}/children", parent_id.0);
203        self.http_provider.get(&path).await.map_err(|e| e.into())
204    }
205
206    async fn get_root_collections(&self) -> RepositoryResult<Vec<Collection>> {
207        self.http_provider
208            .get("/api/collection/root")
209            .await
210            .map_err(|e| e.into())
211    }
212
213    async fn get_by_parent(
214        &self,
215        parent_id: Option<CollectionId>,
216    ) -> RepositoryResult<Vec<Collection>> {
217        let path = match parent_id {
218            Some(id) => format!("/api/collection?parent_id={}", id.0),
219            None => "/api/collection?parent_id=".to_string(),
220        };
221        self.http_provider.get(&path).await.map_err(|e| e.into())
222    }
223
224    async fn get_permissions(&self, id: &CollectionId) -> RepositoryResult<serde_json::Value> {
225        let path = format!("/api/collection/{}/permissions", id.0);
226        self.http_provider.get(&path).await.map_err(|e| e.into())
227    }
228
229    async fn update_permissions(
230        &self,
231        id: &CollectionId,
232        permissions: &serde_json::Value,
233    ) -> RepositoryResult<()> {
234        let path = format!("/api/collection/{}/permissions", id.0);
235        self.http_provider
236            .put(&path, permissions)
237            .await
238            .map(|_: serde_json::Value| ())
239            .map_err(|e| e.into())
240    }
241
242    async fn move_collection(
243        &self,
244        id: &CollectionId,
245        new_parent_id: Option<CollectionId>,
246    ) -> RepositoryResult<Collection> {
247        let path = format!("/api/collection/{}", id.0);
248        let body = serde_json::json!({
249            "parent_id": new_parent_id.map(|id| id.0)
250        });
251        self.http_provider
252            .put(&path, &body)
253            .await
254            .map_err(|e| e.into())
255    }
256
257    async fn archive(&self, id: &CollectionId) -> RepositoryResult<()> {
258        let path = format!("/api/collection/{}", id.0);
259        let body = serde_json::json!({ "archived": true });
260        self.http_provider
261            .put(&path, &body)
262            .await
263            .map(|_: serde_json::Value| ())
264            .map_err(|e| e.into())
265    }
266
267    async fn unarchive(&self, id: &CollectionId) -> RepositoryResult<()> {
268        let path = format!("/api/collection/{}", id.0);
269        let body = serde_json::json!({ "archived": false });
270        self.http_provider
271            .put(&path, &body)
272            .await
273            .map(|_: serde_json::Value| ())
274            .map_err(|e| e.into())
275    }
276}
277
278/// Mock implementation of CollectionRepository for testing
279pub struct MockCollectionRepository {
280    collections: Arc<tokio::sync::RwLock<Vec<Collection>>>,
281    should_fail: bool,
282}
283
284impl MockCollectionRepository {
285    /// Create a new mock collection repository
286    pub fn new() -> Self {
287        Self {
288            collections: Arc::new(tokio::sync::RwLock::new(Vec::new())),
289            should_fail: false,
290        }
291    }
292
293    /// Set whether operations should fail
294    pub fn set_should_fail(&mut self, should_fail: bool) {
295        self.should_fail = should_fail;
296    }
297
298    /// Add a collection to the mock repository
299    pub async fn add_collection(&self, collection: Collection) {
300        let mut collections = self.collections.write().await;
301        collections.push(collection);
302    }
303}
304
305impl Default for MockCollectionRepository {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311#[async_trait]
312impl Repository for MockCollectionRepository {
313    type Entity = Collection;
314    type Id = CollectionId;
315
316    async fn get(&self, id: &CollectionId) -> RepositoryResult<Collection> {
317        if self.should_fail {
318            return Err(RepositoryError::Other("Mock failure".to_string()));
319        }
320
321        let collections = self.collections.read().await;
322        collections
323            .iter()
324            .find(|c| c.id == Some(*id))
325            .cloned()
326            .ok_or_else(|| RepositoryError::NotFound(format!("Collection {} not found", id.0)))
327    }
328
329    async fn list(
330        &self,
331        _pagination: Option<PaginationParams>,
332        _filters: Option<FilterParams>,
333    ) -> RepositoryResult<Vec<Collection>> {
334        if self.should_fail {
335            return Err(RepositoryError::Other("Mock failure".to_string()));
336        }
337
338        let collections = self.collections.read().await;
339        Ok(collections.clone())
340    }
341
342    async fn create(&self, entity: &Collection) -> RepositoryResult<Collection> {
343        if self.should_fail {
344            return Err(RepositoryError::Other("Mock failure".to_string()));
345        }
346
347        let mut collections = self.collections.write().await;
348        let mut new_collection = entity.clone();
349        // Generate a mock ID if not present
350        if new_collection.id.is_none() {
351            new_collection.id = Some(CollectionId((collections.len() + 1) as i32));
352        }
353        collections.push(new_collection.clone());
354        Ok(new_collection)
355    }
356
357    async fn update(&self, id: &CollectionId, entity: &Collection) -> RepositoryResult<Collection> {
358        if self.should_fail {
359            return Err(RepositoryError::Other("Mock failure".to_string()));
360        }
361
362        let mut collections = self.collections.write().await;
363        if let Some(collection) = collections.iter_mut().find(|c| c.id == Some(*id)) {
364            *collection = entity.clone();
365            collection.id = Some(*id); // Ensure ID is preserved
366            Ok(collection.clone())
367        } else {
368            Err(RepositoryError::NotFound(format!(
369                "Collection {} not found",
370                id.0
371            )))
372        }
373    }
374
375    async fn delete(&self, id: &CollectionId) -> RepositoryResult<()> {
376        if self.should_fail {
377            return Err(RepositoryError::Other("Mock failure".to_string()));
378        }
379
380        let mut collections = self.collections.write().await;
381        let initial_len = collections.len();
382        collections.retain(|c| c.id != Some(*id));
383
384        if collections.len() < initial_len {
385            Ok(())
386        } else {
387            Err(RepositoryError::NotFound(format!(
388                "Collection {} not found",
389                id.0
390            )))
391        }
392    }
393}
394
395#[async_trait]
396impl CollectionRepository for MockCollectionRepository {
397    async fn list_with_filters(
398        &self,
399        pagination: Option<PaginationParams>,
400        filters: Option<CollectionFilterParams>,
401    ) -> RepositoryResult<Vec<Collection>> {
402        let base_filters = filters.map(|f| f.base);
403        self.list(pagination, base_filters).await
404    }
405
406    async fn get_children(&self, parent_id: CollectionId) -> RepositoryResult<Vec<Collection>> {
407        if self.should_fail {
408            return Err(RepositoryError::Other("Mock failure".to_string()));
409        }
410
411        let collections = self.collections.read().await;
412        Ok(collections
413            .iter()
414            .filter(|c| c.parent_id == Some(parent_id.0))
415            .cloned()
416            .collect())
417    }
418
419    async fn get_root_collections(&self) -> RepositoryResult<Vec<Collection>> {
420        if self.should_fail {
421            return Err(RepositoryError::Other("Mock failure".to_string()));
422        }
423
424        let collections = self.collections.read().await;
425        Ok(collections
426            .iter()
427            .filter(|c| c.parent_id.is_none())
428            .cloned()
429            .collect())
430    }
431
432    async fn get_by_parent(
433        &self,
434        parent_id: Option<CollectionId>,
435    ) -> RepositoryResult<Vec<Collection>> {
436        if self.should_fail {
437            return Err(RepositoryError::Other("Mock failure".to_string()));
438        }
439
440        let collections = self.collections.read().await;
441        Ok(collections
442            .iter()
443            .filter(|c| match parent_id {
444                Some(id) => c.parent_id == Some(id.0),
445                None => c.parent_id.is_none(),
446            })
447            .cloned()
448            .collect())
449    }
450
451    async fn get_permissions(&self, id: &CollectionId) -> RepositoryResult<serde_json::Value> {
452        if self.should_fail {
453            return Err(RepositoryError::Other("Mock failure".to_string()));
454        }
455
456        // Verify collection exists
457        self.get(id).await?;
458
459        // Return mock permissions
460        Ok(serde_json::json!({
461            "read": ["all"],
462            "write": ["admin"],
463        }))
464    }
465
466    async fn update_permissions(
467        &self,
468        id: &CollectionId,
469        _permissions: &serde_json::Value,
470    ) -> RepositoryResult<()> {
471        if self.should_fail {
472            return Err(RepositoryError::Other("Mock failure".to_string()));
473        }
474
475        // Verify collection exists
476        self.get(id).await?;
477
478        // In a real implementation, we would store the permissions
479        Ok(())
480    }
481
482    async fn move_collection(
483        &self,
484        id: &CollectionId,
485        new_parent_id: Option<CollectionId>,
486    ) -> RepositoryResult<Collection> {
487        if self.should_fail {
488            return Err(RepositoryError::Other("Mock failure".to_string()));
489        }
490
491        let mut collections = self.collections.write().await;
492        if let Some(collection) = collections.iter_mut().find(|c| c.id == Some(*id)) {
493            collection.parent_id = new_parent_id.map(|id| id.0);
494            Ok(collection.clone())
495        } else {
496            Err(RepositoryError::NotFound(format!(
497                "Collection {} not found",
498                id.0
499            )))
500        }
501    }
502
503    async fn archive(&self, id: &CollectionId) -> RepositoryResult<()> {
504        if self.should_fail {
505            return Err(RepositoryError::Other("Mock failure".to_string()));
506        }
507
508        let mut collections = self.collections.write().await;
509        if let Some(collection) = collections.iter_mut().find(|c| c.id == Some(*id)) {
510            collection.archived = Some(true);
511            Ok(())
512        } else {
513            Err(RepositoryError::NotFound(format!(
514                "Collection {} not found",
515                id.0
516            )))
517        }
518    }
519
520    async fn unarchive(&self, id: &CollectionId) -> RepositoryResult<()> {
521        if self.should_fail {
522            return Err(RepositoryError::Other("Mock failure".to_string()));
523        }
524
525        let mut collections = self.collections.write().await;
526        if let Some(collection) = collections.iter_mut().find(|c| c.id == Some(*id)) {
527            collection.archived = Some(false);
528            Ok(())
529        } else {
530            Err(RepositoryError::NotFound(format!(
531                "Collection {} not found",
532                id.0
533            )))
534        }
535    }
536}