1use crate::core::pluralize::Pluralizer;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct EntityReference {
11 pub id: Uuid,
13
14 pub entity_type: String,
19}
20
21impl EntityReference {
22 pub fn new(id: Uuid, entity_type: impl Into<String>) -> Self {
24 Self {
25 id,
26 entity_type: entity_type.into(),
27 }
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Link {
38 pub id: Uuid,
40
41 pub tenant_id: Uuid,
43
44 pub link_type: String,
49
50 pub source: EntityReference,
52
53 pub target: EntityReference,
55
56 pub metadata: Option<serde_json::Value>,
63
64 pub created_at: DateTime<Utc>,
66
67 pub updated_at: DateTime<Utc>,
69}
70
71impl Link {
72 pub fn new(
74 tenant_id: Uuid,
75 link_type: impl Into<String>,
76 source: EntityReference,
77 target: EntityReference,
78 metadata: Option<serde_json::Value>,
79 ) -> Self {
80 let now = Utc::now();
81 Self {
82 id: Uuid::new_v4(),
83 tenant_id,
84 link_type: link_type.into(),
85 source,
86 target,
87 metadata,
88 created_at: now,
89 updated_at: now,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct LinkAuthConfig {
100 #[serde(default = "default_link_auth_policy")]
103 pub list: String,
104
105 #[serde(default = "default_link_auth_policy")]
108 pub get: String,
109
110 #[serde(default = "default_link_auth_policy")]
113 pub create: String,
114
115 #[serde(default = "default_link_auth_policy")]
118 pub update: String,
119
120 #[serde(default = "default_link_auth_policy")]
123 pub delete: String,
124}
125
126fn default_link_auth_policy() -> String {
127 "authenticated".to_string()
128}
129
130impl Default for LinkAuthConfig {
131 fn default() -> Self {
132 Self {
133 list: default_link_auth_policy(),
134 get: default_link_auth_policy(),
135 create: default_link_auth_policy(),
136 update: default_link_auth_policy(),
137 delete: default_link_auth_policy(),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct LinkDefinition {
148 pub link_type: String,
150
151 pub source_type: String,
153
154 pub target_type: String,
156
157 pub forward_route_name: String,
161
162 pub reverse_route_name: String,
166
167 pub description: Option<String>,
169
170 pub required_fields: Option<Vec<String>>,
172
173 #[serde(default)]
183 pub auth: Option<LinkAuthConfig>,
184}
185
186impl LinkDefinition {
187 pub fn default_forward_route_name(target_type: &str, link_type: &str) -> String {
192 format!(
193 "{}-{}",
194 Pluralizer::pluralize(target_type),
195 Pluralizer::pluralize(link_type)
196 )
197 }
198
199 pub fn default_reverse_route_name(source_type: &str, link_type: &str) -> String {
204 format!(
205 "{}-{}",
206 Pluralizer::pluralize(source_type),
207 Pluralizer::pluralize(link_type)
208 )
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_entity_reference_creation() {
218 let user_id = Uuid::new_v4();
219 let reference = EntityReference::new(user_id, "user");
220
221 assert_eq!(reference.id, user_id);
222 assert_eq!(reference.entity_type, "user");
223 }
224
225 #[test]
226 fn test_link_creation() {
227 let tenant_id = Uuid::new_v4();
228 let user_id = Uuid::new_v4();
229 let car_id = Uuid::new_v4();
230
231 let link = Link::new(
232 tenant_id,
233 "owner",
234 EntityReference::new(user_id, "user"),
235 EntityReference::new(car_id, "car"),
236 None,
237 );
238
239 assert_eq!(link.tenant_id, tenant_id);
240 assert_eq!(link.link_type, "owner");
241 assert_eq!(link.source.id, user_id);
242 assert_eq!(link.target.id, car_id);
243 assert!(link.metadata.is_none());
244 }
245
246 #[test]
247 fn test_link_with_metadata() {
248 let tenant_id = Uuid::new_v4();
249 let user_id = Uuid::new_v4();
250 let company_id = Uuid::new_v4();
251
252 let metadata = serde_json::json!({
253 "role": "Senior Developer",
254 "start_date": "2024-01-01"
255 });
256
257 let link = Link::new(
258 tenant_id,
259 "worker",
260 EntityReference::new(user_id, "user"),
261 EntityReference::new(company_id, "company"),
262 Some(metadata.clone()),
263 );
264
265 assert_eq!(link.metadata, Some(metadata));
266 }
267
268 #[test]
269 fn test_default_route_names() {
270 let forward = LinkDefinition::default_forward_route_name("car", "owner");
271 assert_eq!(forward, "cars-owners");
272
273 let reverse = LinkDefinition::default_reverse_route_name("user", "owner");
274 assert_eq!(reverse, "users-owners");
275 }
276
277 #[test]
278 fn test_route_names_with_irregular_plurals() {
279 let forward = LinkDefinition::default_forward_route_name("company", "owner");
280 assert_eq!(forward, "companies-owners");
281
282 let reverse = LinkDefinition::default_reverse_route_name("company", "worker");
283 assert_eq!(reverse, "companies-workers");
284 }
285
286 #[test]
287 fn test_link_auth_config_default() {
288 let auth = LinkAuthConfig::default();
289 assert_eq!(auth.list, "authenticated");
290 assert_eq!(auth.get, "authenticated");
291 assert_eq!(auth.create, "authenticated");
292 assert_eq!(auth.update, "authenticated");
293 assert_eq!(auth.delete, "authenticated");
294 }
295
296 #[test]
297 fn test_link_definition_with_auth() {
298 let yaml = r#"
299 link_type: has_invoice
300 source_type: order
301 target_type: invoice
302 forward_route_name: invoices
303 reverse_route_name: order
304 auth:
305 list: authenticated
306 get: owner
307 create: service_only
308 update: owner
309 delete: admin_only
310 "#;
311
312 let def: LinkDefinition = serde_yaml::from_str(yaml).unwrap();
313 assert_eq!(def.link_type, "has_invoice");
314 assert_eq!(def.source_type, "order");
315 assert_eq!(def.target_type, "invoice");
316
317 let auth = def.auth.unwrap();
318 assert_eq!(auth.list, "authenticated");
319 assert_eq!(auth.get, "owner");
320 assert_eq!(auth.create, "service_only");
321 assert_eq!(auth.update, "owner");
322 assert_eq!(auth.delete, "admin_only");
323 }
324
325 #[test]
326 fn test_link_definition_without_auth() {
327 let yaml = r#"
328 link_type: payment
329 source_type: invoice
330 target_type: payment
331 forward_route_name: payments
332 reverse_route_name: invoice
333 "#;
334
335 let def: LinkDefinition = serde_yaml::from_str(yaml).unwrap();
336 assert_eq!(def.link_type, "payment");
337 assert!(def.auth.is_none());
338 }
339}