1use crate::core::pluralize::Pluralizer;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct LinkEntity {
14 pub id: Uuid,
16
17 #[serde(rename = "type")]
19 pub entity_type: String,
20
21 pub created_at: DateTime<Utc>,
23
24 pub updated_at: DateTime<Utc>,
26
27 pub deleted_at: Option<DateTime<Utc>>,
29
30 pub status: String,
32
33 pub link_type: String,
35
36 pub source_id: Uuid,
38
39 pub target_id: Uuid,
41
42 pub metadata: Option<serde_json::Value>,
44}
45
46impl LinkEntity {
47 pub fn new(
49 link_type: impl Into<String>,
50 source_id: Uuid,
51 target_id: Uuid,
52 metadata: Option<serde_json::Value>,
53 ) -> Self {
54 let now = Utc::now();
55 Self {
56 id: Uuid::new_v4(),
57 entity_type: "link".to_string(),
58 created_at: now,
59 updated_at: now,
60 deleted_at: None,
61 status: "active".to_string(),
62 link_type: link_type.into(),
63 source_id,
64 target_id,
65 metadata,
66 }
67 }
68
69 pub fn soft_delete(&mut self) {
71 self.deleted_at = Some(Utc::now());
72 self.updated_at = Utc::now();
73 }
74
75 pub fn restore(&mut self) {
77 self.deleted_at = None;
78 self.updated_at = Utc::now();
79 }
80
81 pub fn touch(&mut self) {
83 self.updated_at = Utc::now();
84 }
85
86 pub fn is_deleted(&self) -> bool {
88 self.deleted_at.is_some()
89 }
90
91 pub fn is_active(&self) -> bool {
93 self.status == "active" && !self.is_deleted()
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct LinkAuthConfig {
103 #[serde(default = "default_link_auth_policy")]
105 pub list: String,
106
107 #[serde(default = "default_link_auth_policy")]
109 pub get: String,
110
111 #[serde(default = "default_link_auth_policy")]
113 pub create: String,
114
115 #[serde(default = "default_link_auth_policy")]
117 pub update: String,
118
119 #[serde(default = "default_link_auth_policy")]
121 pub delete: String,
122}
123
124fn default_link_auth_policy() -> String {
125 "authenticated".to_string()
126}
127
128impl Default for LinkAuthConfig {
129 fn default() -> Self {
130 Self {
131 list: default_link_auth_policy(),
132 get: default_link_auth_policy(),
133 create: default_link_auth_policy(),
134 update: default_link_auth_policy(),
135 delete: default_link_auth_policy(),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct LinkDefinition {
143 pub link_type: String,
145
146 pub source_type: String,
148
149 pub target_type: String,
151
152 pub forward_route_name: String,
154
155 pub reverse_route_name: String,
157
158 pub description: Option<String>,
160
161 pub required_fields: Option<Vec<String>>,
163
164 #[serde(default)]
166 pub auth: Option<LinkAuthConfig>,
167}
168
169impl LinkDefinition {
170 pub fn default_forward_route_name(target_type: &str, link_type: &str) -> String {
172 format!(
173 "{}-{}",
174 Pluralizer::pluralize(target_type),
175 Pluralizer::pluralize(link_type)
176 )
177 }
178
179 pub fn default_reverse_route_name(source_type: &str, link_type: &str) -> String {
181 format!(
182 "{}-{}",
183 Pluralizer::pluralize(source_type),
184 Pluralizer::pluralize(link_type)
185 )
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_link_creation() {
195 let user_id = Uuid::new_v4();
196 let car_id = Uuid::new_v4();
197
198 let link = LinkEntity::new("owner", user_id, car_id, None);
199
200 assert_eq!(link.link_type, "owner");
201 assert_eq!(link.source_id, user_id);
202 assert_eq!(link.target_id, car_id);
203 assert!(link.metadata.is_none());
204 assert_eq!(link.status, "active");
205 assert!(!link.is_deleted());
206 assert!(link.is_active());
207 }
208
209 #[test]
210 fn test_link_with_metadata() {
211 let user_id = Uuid::new_v4();
212 let company_id = Uuid::new_v4();
213
214 let metadata = serde_json::json!({
215 "role": "Senior Developer",
216 "start_date": "2024-01-01"
217 });
218
219 let link = LinkEntity::new("worker", user_id, company_id, Some(metadata.clone()));
220
221 assert_eq!(link.metadata, Some(metadata));
222 }
223
224 #[test]
225 fn test_link_soft_delete() {
226 let mut link = LinkEntity::new("owner", Uuid::new_v4(), Uuid::new_v4(), None);
227
228 assert!(!link.is_deleted());
229 assert!(link.is_active());
230
231 link.soft_delete();
232 assert!(link.is_deleted());
233 assert!(!link.is_active());
234 }
235
236 #[test]
237 fn test_link_restore() {
238 let mut link = LinkEntity::new("owner", Uuid::new_v4(), Uuid::new_v4(), None);
239
240 link.soft_delete();
241 assert!(link.is_deleted());
242
243 link.restore();
244 assert!(!link.is_deleted());
245 assert!(link.is_active());
246 }
247
248 #[test]
249 fn test_default_route_names() {
250 let forward = LinkDefinition::default_forward_route_name("car", "owner");
251 assert_eq!(forward, "cars-owners");
252
253 let reverse = LinkDefinition::default_reverse_route_name("user", "owner");
254 assert_eq!(reverse, "users-owners");
255 }
256
257 #[test]
258 fn test_route_names_with_irregular_plurals() {
259 let forward = LinkDefinition::default_forward_route_name("company", "owner");
260 assert_eq!(forward, "companies-owners");
261
262 let reverse = LinkDefinition::default_reverse_route_name("company", "worker");
263 assert_eq!(reverse, "companies-workers");
264 }
265
266 #[test]
267 fn test_link_auth_config_default() {
268 let auth = LinkAuthConfig::default();
269 assert_eq!(auth.list, "authenticated");
270 assert_eq!(auth.get, "authenticated");
271 assert_eq!(auth.create, "authenticated");
272 assert_eq!(auth.update, "authenticated");
273 assert_eq!(auth.delete, "authenticated");
274 }
275
276 #[test]
277 fn test_link_definition_with_auth() {
278 let yaml = r#"
279 link_type: has_invoice
280 source_type: order
281 target_type: invoice
282 forward_route_name: invoices
283 reverse_route_name: order
284 auth:
285 list: authenticated
286 get: owner
287 create: service_only
288 update: owner
289 delete: admin_only
290 "#;
291
292 let def: LinkDefinition = serde_yaml::from_str(yaml).unwrap();
293 assert_eq!(def.link_type, "has_invoice");
294 assert_eq!(def.source_type, "order");
295 assert_eq!(def.target_type, "invoice");
296
297 let auth = def.auth.unwrap();
298 assert_eq!(auth.list, "authenticated");
299 assert_eq!(auth.get, "owner");
300 assert_eq!(auth.create, "service_only");
301 assert_eq!(auth.update, "owner");
302 assert_eq!(auth.delete, "admin_only");
303 }
304}