metabase_api_rs/service/
collection.rs

1//! Collection service implementation
2//!
3//! This module provides business logic for Collection operations.
4
5use super::traits::{Service, ServiceError, ServiceResult, ValidationContext};
6use crate::core::models::{common::CollectionId, Collection};
7use crate::repository::{
8    collection::{CollectionFilterParams, CollectionRepository},
9    traits::PaginationParams,
10};
11use async_trait::async_trait;
12use std::sync::Arc;
13
14/// Service trait for Collection operations
15#[async_trait]
16pub trait CollectionService: Service {
17    /// Get a collection by ID
18    async fn get_collection(&self, id: CollectionId) -> ServiceResult<Collection>;
19
20    /// List collections with filters
21    async fn list_collections(
22        &self,
23        pagination: Option<PaginationParams>,
24        filters: Option<CollectionFilterParams>,
25    ) -> ServiceResult<Vec<Collection>>;
26
27    /// Create a new collection
28    async fn create_collection(&self, collection: Collection) -> ServiceResult<Collection>;
29
30    /// Update a collection
31    async fn update_collection(
32        &self,
33        id: CollectionId,
34        collection: Collection,
35    ) -> ServiceResult<Collection>;
36
37    /// Delete a collection
38    async fn delete_collection(&self, id: CollectionId) -> ServiceResult<()>;
39
40    /// Archive a collection
41    async fn archive_collection(&self, id: CollectionId) -> ServiceResult<()>;
42
43    /// Unarchive a collection
44    async fn unarchive_collection(&self, id: CollectionId) -> ServiceResult<()>;
45
46    /// Move a collection to a new parent
47    async fn move_collection(
48        &self,
49        id: CollectionId,
50        new_parent_id: Option<CollectionId>,
51    ) -> ServiceResult<Collection>;
52
53    /// Get root collections
54    async fn get_root_collections(&self) -> ServiceResult<Vec<Collection>>;
55
56    /// Get collections by parent
57    async fn get_collections_by_parent(
58        &self,
59        parent_id: CollectionId,
60    ) -> ServiceResult<Vec<Collection>>;
61
62    /// Validate collection data
63    async fn validate_collection(&self, collection: &Collection) -> ServiceResult<()>;
64}
65
66/// HTTP implementation of CollectionService
67pub struct HttpCollectionService {
68    repository: Arc<dyn CollectionRepository>,
69}
70
71impl HttpCollectionService {
72    /// Create a new HTTP collection service
73    pub fn new(repository: Arc<dyn CollectionRepository>) -> Self {
74        Self { repository }
75    }
76
77    /// Validate collection business rules
78    fn validate_collection_rules(&self, collection: &Collection) -> ServiceResult<()> {
79        let mut context = ValidationContext::new();
80
81        // Name validation
82        if collection.name.trim().is_empty() {
83            context.add_error("Collection name cannot be empty");
84        }
85
86        if collection.name.len() > 255 {
87            context.add_error("Collection name cannot exceed 255 characters");
88        }
89
90        // Description validation
91        if let Some(desc) = &collection.description {
92            if desc.len() > 5000 {
93                context.add_error("Collection description cannot exceed 5000 characters");
94            }
95        }
96
97        // Color validation
98        if let Some(color) = &collection.color {
99            // Validate hex color format
100            if !color.starts_with('#') || color.len() != 7 {
101                context.add_error("Collection color must be in hex format (#RRGGBB)");
102            }
103        }
104
105        // Slug validation
106        if let Some(slug) = &collection.slug {
107            if slug.contains(' ') {
108                context.add_error("Collection slug cannot contain spaces");
109            }
110            if slug.len() > 100 {
111                context.add_error("Collection slug cannot exceed 100 characters");
112            }
113        }
114
115        context.to_result()
116    }
117
118    /// Check for circular references in collection hierarchy
119    async fn check_circular_reference(
120        &self,
121        id: CollectionId,
122        parent_id: Option<CollectionId>,
123    ) -> ServiceResult<()> {
124        if let Some(parent) = parent_id {
125            if parent == id {
126                return Err(ServiceError::BusinessRule(
127                    "Cannot set collection as its own parent".to_string(),
128                ));
129            }
130
131            // TODO: Implement full circular reference check by traversing the hierarchy
132        }
133        Ok(())
134    }
135}
136
137#[async_trait]
138impl Service for HttpCollectionService {
139    fn name(&self) -> &str {
140        "CollectionService"
141    }
142}
143
144#[async_trait]
145impl CollectionService for HttpCollectionService {
146    async fn get_collection(&self, id: CollectionId) -> ServiceResult<Collection> {
147        self.repository.get(&id).await.map_err(ServiceError::from)
148    }
149
150    async fn list_collections(
151        &self,
152        pagination: Option<PaginationParams>,
153        filters: Option<CollectionFilterParams>,
154    ) -> ServiceResult<Vec<Collection>> {
155        self.repository
156            .list_with_filters(pagination, filters)
157            .await
158            .map_err(ServiceError::from)
159    }
160
161    async fn create_collection(&self, collection: Collection) -> ServiceResult<Collection> {
162        // Validate business rules
163        self.validate_collection_rules(&collection)?;
164
165        // Check parent exists if specified
166        if let Some(parent_id) = collection.parent_id {
167            self.repository
168                .get(&CollectionId(parent_id))
169                .await
170                .map_err(|_| {
171                    ServiceError::NotFound(format!("Parent collection {} not found", parent_id))
172                })?;
173        }
174
175        // Create via repository
176        self.repository
177            .create(&collection)
178            .await
179            .map_err(ServiceError::from)
180    }
181
182    async fn update_collection(
183        &self,
184        id: CollectionId,
185        mut collection: Collection,
186    ) -> ServiceResult<Collection> {
187        // Ensure ID matches
188        collection.id = Some(id);
189
190        // Validate business rules
191        self.validate_collection_rules(&collection)?;
192
193        // Check if collection exists
194        self.repository.get(&id).await.map_err(ServiceError::from)?;
195
196        // Check for circular reference if parent is being changed
197        if let Some(parent_id) = collection.parent_id {
198            self.check_circular_reference(id, Some(CollectionId(parent_id)))
199                .await?;
200        }
201
202        // Update via repository
203        self.repository
204            .update(&id, &collection)
205            .await
206            .map_err(ServiceError::from)
207    }
208
209    async fn delete_collection(&self, id: CollectionId) -> ServiceResult<()> {
210        // Check if collection exists
211        let collection = self.repository.get(&id).await.map_err(ServiceError::from)?;
212
213        // Check if collection is personal
214        if collection.is_personal() {
215            return Err(ServiceError::BusinessRule(
216                "Cannot delete personal collections".to_string(),
217            ));
218        }
219
220        // TODO: Check for child collections and items
221
222        // Delete via repository
223        self.repository
224            .delete(&id)
225            .await
226            .map_err(ServiceError::from)
227    }
228
229    async fn archive_collection(&self, id: CollectionId) -> ServiceResult<()> {
230        self.repository
231            .archive(&id)
232            .await
233            .map_err(ServiceError::from)
234    }
235
236    async fn unarchive_collection(&self, id: CollectionId) -> ServiceResult<()> {
237        self.repository
238            .unarchive(&id)
239            .await
240            .map_err(ServiceError::from)
241    }
242
243    async fn move_collection(
244        &self,
245        id: CollectionId,
246        new_parent_id: Option<CollectionId>,
247    ) -> ServiceResult<Collection> {
248        // Check for circular reference
249        self.check_circular_reference(id, new_parent_id).await?;
250
251        // Check if new parent exists
252        if let Some(parent) = new_parent_id {
253            self.repository.get(&parent).await.map_err(|_| {
254                ServiceError::NotFound(format!("Parent collection {} not found", parent.0))
255            })?;
256        }
257
258        self.repository
259            .move_collection(&id, new_parent_id)
260            .await
261            .map_err(ServiceError::from)
262    }
263
264    async fn get_root_collections(&self) -> ServiceResult<Vec<Collection>> {
265        self.repository
266            .get_root_collections()
267            .await
268            .map_err(ServiceError::from)
269    }
270
271    async fn get_collections_by_parent(
272        &self,
273        parent_id: CollectionId,
274    ) -> ServiceResult<Vec<Collection>> {
275        self.repository
276            .get_by_parent(Some(parent_id))
277            .await
278            .map_err(ServiceError::from)
279    }
280
281    async fn validate_collection(&self, collection: &Collection) -> ServiceResult<()> {
282        self.validate_collection_rules(collection)
283    }
284}