this/links/
service.rs

1//! In-memory implementation of LinkService for testing and development
2
3use crate::core::{EntityReference, Link, LinkService};
4use anyhow::{anyhow, Result};
5use async_trait::async_trait;
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8use uuid::Uuid;
9
10/// In-memory link service implementation
11///
12/// Useful for testing and development. Uses RwLock for thread-safe access.
13#[derive(Clone)]
14pub struct InMemoryLinkService {
15    links: Arc<RwLock<HashMap<Uuid, Link>>>,
16}
17
18impl InMemoryLinkService {
19    /// Create a new in-memory link service
20    pub fn new() -> Self {
21        Self {
22            links: Arc::new(RwLock::new(HashMap::new())),
23        }
24    }
25}
26
27impl Default for InMemoryLinkService {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33#[async_trait]
34impl LinkService for InMemoryLinkService {
35    async fn create(
36        &self,
37        tenant_id: &Uuid,
38        link_type: &str,
39        source: EntityReference,
40        target: EntityReference,
41        metadata: Option<serde_json::Value>,
42    ) -> Result<Link> {
43        let link = Link::new(*tenant_id, link_type, source, target, metadata);
44
45        let mut links = self
46            .links
47            .write()
48            .map_err(|e| anyhow!("Failed to acquire write lock: {}", e))?;
49
50        links.insert(link.id, link.clone());
51
52        Ok(link)
53    }
54
55    async fn get(&self, tenant_id: &Uuid, id: &Uuid) -> Result<Option<Link>> {
56        let links = self
57            .links
58            .read()
59            .map_err(|e| anyhow!("Failed to acquire read lock: {}", e))?;
60
61        Ok(links
62            .get(id)
63            .filter(|link| &link.tenant_id == tenant_id)
64            .cloned())
65    }
66
67    async fn list(&self, tenant_id: &Uuid) -> Result<Vec<Link>> {
68        let links = self
69            .links
70            .read()
71            .map_err(|e| anyhow!("Failed to acquire read lock: {}", e))?;
72
73        Ok(links
74            .values()
75            .filter(|link| &link.tenant_id == tenant_id)
76            .cloned()
77            .collect())
78    }
79
80    async fn find_by_source(
81        &self,
82        tenant_id: &Uuid,
83        source_id: &Uuid,
84        source_type: &str,
85        link_type: Option<&str>,
86        target_type: Option<&str>,
87    ) -> Result<Vec<Link>> {
88        let links = self
89            .links
90            .read()
91            .map_err(|e| anyhow!("Failed to acquire read lock: {}", e))?;
92
93        Ok(links
94            .values()
95            .filter(|link| {
96                &link.tenant_id == tenant_id
97                    && &link.source.id == source_id
98                    && link.source.entity_type == source_type
99                    && link_type.is_none_or(|lt| link.link_type == lt)
100                    && target_type.is_none_or(|tt| link.target.entity_type == tt)
101            })
102            .cloned()
103            .collect())
104    }
105
106    async fn find_by_target(
107        &self,
108        tenant_id: &Uuid,
109        target_id: &Uuid,
110        target_type: &str,
111        link_type: Option<&str>,
112        source_type: Option<&str>,
113    ) -> Result<Vec<Link>> {
114        let links = self
115            .links
116            .read()
117            .map_err(|e| anyhow!("Failed to acquire read lock: {}", e))?;
118
119        Ok(links
120            .values()
121            .filter(|link| {
122                &link.tenant_id == tenant_id
123                    && &link.target.id == target_id
124                    && link.target.entity_type == target_type
125                    && link_type.is_none_or(|lt| link.link_type == lt)
126                    && source_type.is_none_or(|st| link.source.entity_type == st)
127            })
128            .cloned()
129            .collect())
130    }
131
132    async fn update(
133        &self,
134        tenant_id: &Uuid,
135        id: &Uuid,
136        metadata: Option<serde_json::Value>,
137    ) -> Result<Link> {
138        let mut links = self
139            .links
140            .write()
141            .map_err(|e| anyhow!("Failed to acquire write lock: {}", e))?;
142
143        let link = links.get_mut(id).ok_or_else(|| anyhow!("Link not found"))?;
144
145        // Verify tenant ownership
146        if &link.tenant_id != tenant_id {
147            return Err(anyhow!("Link not found or access denied"));
148        }
149
150        // Update metadata and timestamp
151        link.metadata = metadata;
152        link.updated_at = chrono::Utc::now();
153
154        Ok(link.clone())
155    }
156
157    async fn delete(&self, tenant_id: &Uuid, id: &Uuid) -> Result<()> {
158        let mut links = self
159            .links
160            .write()
161            .map_err(|e| anyhow!("Failed to acquire write lock: {}", e))?;
162
163        if let Some(link) = links.get(id) {
164            if &link.tenant_id != tenant_id {
165                return Err(anyhow!("Link not found or access denied"));
166            }
167            links.remove(id);
168        }
169
170        Ok(())
171    }
172
173    async fn delete_by_entity(
174        &self,
175        tenant_id: &Uuid,
176        entity_id: &Uuid,
177        entity_type: &str,
178    ) -> Result<()> {
179        let mut links = self
180            .links
181            .write()
182            .map_err(|e| anyhow!("Failed to acquire write lock: {}", e))?;
183
184        links.retain(|_, link| {
185            &link.tenant_id != tenant_id
186                || (&link.source.id != entity_id || link.source.entity_type != entity_type)
187                    && (&link.target.id != entity_id || link.target.entity_type != entity_type)
188        });
189
190        Ok(())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[tokio::test]
199    async fn test_create_link() {
200        let service = InMemoryLinkService::new();
201        let tenant_id = Uuid::new_v4();
202        let user_id = Uuid::new_v4();
203        let car_id = Uuid::new_v4();
204
205        let link = service
206            .create(
207                &tenant_id,
208                "owner",
209                EntityReference::new(user_id, "user"),
210                EntityReference::new(car_id, "car"),
211                None,
212            )
213            .await
214            .unwrap();
215
216        assert_eq!(link.tenant_id, tenant_id);
217        assert_eq!(link.link_type, "owner");
218        assert_eq!(link.source.id, user_id);
219        assert_eq!(link.target.id, car_id);
220    }
221
222    #[tokio::test]
223    async fn test_find_by_source() {
224        let service = InMemoryLinkService::new();
225        let tenant_id = Uuid::new_v4();
226        let user_id = Uuid::new_v4();
227        let car1_id = Uuid::new_v4();
228        let car2_id = Uuid::new_v4();
229
230        // User owns car1
231        service
232            .create(
233                &tenant_id,
234                "owner",
235                EntityReference::new(user_id, "user"),
236                EntityReference::new(car1_id, "car"),
237                None,
238            )
239            .await
240            .unwrap();
241
242        // User drives car2
243        service
244            .create(
245                &tenant_id,
246                "driver",
247                EntityReference::new(user_id, "user"),
248                EntityReference::new(car2_id, "car"),
249                None,
250            )
251            .await
252            .unwrap();
253
254        // Find all links from user
255        let links = service
256            .find_by_source(&tenant_id, &user_id, "user", None, None)
257            .await
258            .unwrap();
259
260        assert_eq!(links.len(), 2);
261
262        // Find only owner links
263        let owner_links = service
264            .find_by_source(&tenant_id, &user_id, "user", Some("owner"), None)
265            .await
266            .unwrap();
267
268        assert_eq!(owner_links.len(), 1);
269        assert_eq!(owner_links[0].link_type, "owner");
270    }
271
272    #[tokio::test]
273    async fn test_update_link() {
274        let service = InMemoryLinkService::new();
275        let tenant_id = Uuid::new_v4();
276        let user_id = Uuid::new_v4();
277        let company_id = Uuid::new_v4();
278
279        // Create a link with initial metadata
280        let initial_metadata = serde_json::json!({
281            "role": "Developer",
282            "start_date": "2024-01-01"
283        });
284
285        let link = service
286            .create(
287                &tenant_id,
288                "worker",
289                EntityReference::new(user_id, "user"),
290                EntityReference::new(company_id, "company"),
291                Some(initial_metadata.clone()),
292            )
293            .await
294            .unwrap();
295
296        assert_eq!(link.metadata, Some(initial_metadata));
297
298        // Update the metadata
299        let updated_metadata = serde_json::json!({
300            "role": "Senior Developer",
301            "start_date": "2024-01-01",
302            "promotion_date": "2024-06-01"
303        });
304
305        let updated_link = service
306            .update(&tenant_id, &link.id, Some(updated_metadata.clone()))
307            .await
308            .unwrap();
309
310        assert_eq!(updated_link.metadata, Some(updated_metadata.clone()));
311        assert_ne!(updated_link.updated_at, link.updated_at);
312
313        // Verify the update persisted
314        let fetched = service.get(&tenant_id, &link.id).await.unwrap();
315        assert!(fetched.is_some());
316        assert_eq!(fetched.unwrap().metadata, Some(updated_metadata));
317    }
318
319    #[tokio::test]
320    async fn test_update_link_removes_metadata() {
321        let service = InMemoryLinkService::new();
322        let tenant_id = Uuid::new_v4();
323        let user_id = Uuid::new_v4();
324        let car_id = Uuid::new_v4();
325
326        // Create link with metadata
327        let link = service
328            .create(
329                &tenant_id,
330                "owner",
331                EntityReference::new(user_id, "user"),
332                EntityReference::new(car_id, "car"),
333                Some(serde_json::json!({"purchase_date": "2024-01-01"})),
334            )
335            .await
336            .unwrap();
337
338        assert!(link.metadata.is_some());
339
340        // Remove metadata by setting to None
341        let updated = service.update(&tenant_id, &link.id, None).await.unwrap();
342
343        assert!(updated.metadata.is_none());
344    }
345
346    #[tokio::test]
347    async fn test_tenant_isolation() {
348        let service = InMemoryLinkService::new();
349        let tenant1_id = Uuid::new_v4();
350        let tenant2_id = Uuid::new_v4();
351        let user_id = Uuid::new_v4();
352        let car_id = Uuid::new_v4();
353
354        // Create link for tenant1
355        let link = service
356            .create(
357                &tenant1_id,
358                "owner",
359                EntityReference::new(user_id, "user"),
360                EntityReference::new(car_id, "car"),
361                None,
362            )
363            .await
364            .unwrap();
365
366        // Tenant1 can see it
367        let result = service.get(&tenant1_id, &link.id).await.unwrap();
368        assert!(result.is_some());
369
370        // Tenant2 cannot see it
371        let result = service.get(&tenant2_id, &link.id).await.unwrap();
372        assert!(result.is_none());
373    }
374
375    #[tokio::test]
376    async fn test_get_link_by_id() {
377        let service = InMemoryLinkService::new();
378        let tenant_id = Uuid::new_v4();
379        let user_id = Uuid::new_v4();
380        let company_id = Uuid::new_v4();
381
382        // Create a link
383        let link = service
384            .create(
385                &tenant_id,
386                "worker",
387                EntityReference::new(user_id, "user"),
388                EntityReference::new(company_id, "company"),
389                Some(serde_json::json!({ "role": "Developer" })),
390            )
391            .await
392            .unwrap();
393
394        // Get the link by ID
395        let retrieved = service.get(&tenant_id, &link.id).await.unwrap();
396        assert!(retrieved.is_some());
397
398        let retrieved_link = retrieved.unwrap();
399        assert_eq!(retrieved_link.id, link.id);
400        assert_eq!(retrieved_link.link_type, "worker");
401        assert_eq!(retrieved_link.source.id, user_id);
402        assert_eq!(retrieved_link.target.id, company_id);
403        assert_eq!(
404            retrieved_link.metadata,
405            Some(serde_json::json!({ "role": "Developer" }))
406        );
407    }
408
409    #[tokio::test]
410    async fn test_get_nonexistent_link() {
411        let service = InMemoryLinkService::new();
412        let tenant_id = Uuid::new_v4();
413        let fake_id = Uuid::new_v4();
414
415        // Try to get a link that doesn't exist
416        let result = service.get(&tenant_id, &fake_id).await.unwrap();
417        assert!(result.is_none());
418    }
419}