1use crate::core::LinkDefinition;
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct EntityAuthConfig {
11 #[serde(default = "default_auth_policy")]
13 pub list: String,
14
15 #[serde(default = "default_auth_policy")]
17 pub get: String,
18
19 #[serde(default = "default_auth_policy")]
21 pub create: String,
22
23 #[serde(default = "default_auth_policy")]
25 pub update: String,
26
27 #[serde(default = "default_auth_policy")]
29 pub delete: String,
30
31 #[serde(default = "default_auth_policy")]
33 pub list_links: String,
34
35 #[serde(default = "default_auth_policy")]
37 pub create_link: String,
38
39 #[serde(default = "default_auth_policy")]
41 pub delete_link: String,
42}
43
44fn default_auth_policy() -> String {
45 "authenticated".to_string()
46}
47
48impl Default for EntityAuthConfig {
49 fn default() -> Self {
50 Self {
51 list: default_auth_policy(),
52 get: default_auth_policy(),
53 create: default_auth_policy(),
54 update: default_auth_policy(),
55 delete: default_auth_policy(),
56 list_links: default_auth_policy(),
57 create_link: default_auth_policy(),
58 delete_link: default_auth_policy(),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct EntityConfig {
66 pub singular: String,
68
69 pub plural: String,
71
72 #[serde(default)]
74 pub auth: EntityAuthConfig,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ValidationRule {
80 pub source: String,
82
83 pub targets: Vec<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct LinksConfig {
90 pub entities: Vec<EntityConfig>,
92
93 pub links: Vec<LinkDefinition>,
95
96 #[serde(default)]
98 pub validation_rules: Option<HashMap<String, Vec<ValidationRule>>>,
99}
100
101impl LinksConfig {
102 pub fn from_yaml_file(path: &str) -> Result<Self> {
104 let content = std::fs::read_to_string(path)?;
105 let config: Self = serde_yaml::from_str(&content)?;
106 Ok(config)
107 }
108
109 pub fn from_yaml_str(yaml: &str) -> Result<Self> {
111 let config: Self = serde_yaml::from_str(yaml)?;
112 Ok(config)
113 }
114
115 pub fn is_valid_link(&self, link_type: &str, source_type: &str, target_type: &str) -> bool {
119 let Some(rules) = &self.validation_rules else {
121 return true;
122 };
123
124 let Some(link_rules) = rules.get(link_type) else {
126 return true; };
128
129 link_rules.iter().any(|rule| {
131 rule.source == source_type && rule.targets.contains(&target_type.to_string())
132 })
133 }
134
135 pub fn find_link_definition(
137 &self,
138 link_type: &str,
139 source_type: &str,
140 target_type: &str,
141 ) -> Option<&LinkDefinition> {
142 self.links.iter().find(|def| {
143 def.link_type == link_type
144 && def.source_type == source_type
145 && def.target_type == target_type
146 })
147 }
148
149 pub fn default_config() -> Self {
151 Self {
152 entities: vec![
153 EntityConfig {
154 singular: "user".to_string(),
155 plural: "users".to_string(),
156 auth: EntityAuthConfig::default(),
157 },
158 EntityConfig {
159 singular: "company".to_string(),
160 plural: "companies".to_string(),
161 auth: EntityAuthConfig::default(),
162 },
163 EntityConfig {
164 singular: "car".to_string(),
165 plural: "cars".to_string(),
166 auth: EntityAuthConfig::default(),
167 },
168 ],
169 links: vec![
170 LinkDefinition {
171 link_type: "owner".to_string(),
172 source_type: "user".to_string(),
173 target_type: "car".to_string(),
174 forward_route_name: "cars-owned".to_string(),
175 reverse_route_name: "users-owners".to_string(),
176 description: Some("User owns a car".to_string()),
177 required_fields: None,
178 auth: None,
179 },
180 LinkDefinition {
181 link_type: "driver".to_string(),
182 source_type: "user".to_string(),
183 target_type: "car".to_string(),
184 forward_route_name: "cars-driven".to_string(),
185 reverse_route_name: "users-drivers".to_string(),
186 description: Some("User drives a car".to_string()),
187 required_fields: None,
188 auth: None,
189 },
190 LinkDefinition {
191 link_type: "worker".to_string(),
192 source_type: "user".to_string(),
193 target_type: "company".to_string(),
194 forward_route_name: "companies-work".to_string(),
195 reverse_route_name: "users-workers".to_string(),
196 description: Some("User works at a company".to_string()),
197 required_fields: Some(vec!["role".to_string()]),
198 auth: None,
199 },
200 ],
201 validation_rules: None,
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_default_config() {
212 let config = LinksConfig::default_config();
213
214 assert_eq!(config.entities.len(), 3);
215 assert_eq!(config.links.len(), 3);
216 }
217
218 #[test]
219 fn test_yaml_serialization() {
220 let config = LinksConfig::default_config();
221 let yaml = serde_yaml::to_string(&config).unwrap();
222
223 let parsed = LinksConfig::from_yaml_str(&yaml).unwrap();
225 assert_eq!(parsed.entities.len(), config.entities.len());
226 assert_eq!(parsed.links.len(), config.links.len());
227 }
228
229 #[test]
230 fn test_link_auth_config_parsing() {
231 let yaml = r#"
232entities:
233 - singular: order
234 plural: orders
235 - singular: invoice
236 plural: invoices
237
238links:
239 - link_type: has_invoice
240 source_type: order
241 target_type: invoice
242 forward_route_name: invoices
243 reverse_route_name: order
244 auth:
245 list: authenticated
246 create: service_only
247 delete: admin_only
248"#;
249
250 let config = LinksConfig::from_yaml_str(yaml).unwrap();
251 assert_eq!(config.links.len(), 1);
252
253 let link_def = &config.links[0];
254 assert!(link_def.auth.is_some());
255
256 let auth = link_def.auth.as_ref().unwrap();
257 assert_eq!(auth.list, "authenticated");
258 assert_eq!(auth.create, "service_only");
259 assert_eq!(auth.delete, "admin_only");
260 }
261
262 #[test]
263 fn test_link_without_auth_config() {
264 let yaml = r#"
265entities:
266 - singular: invoice
267 plural: invoices
268 - singular: payment
269 plural: payments
270
271links:
272 - link_type: payment
273 source_type: invoice
274 target_type: payment
275 forward_route_name: payments
276 reverse_route_name: invoice
277"#;
278
279 let config = LinksConfig::from_yaml_str(yaml).unwrap();
280 assert_eq!(config.links.len(), 1);
281
282 let link_def = &config.links[0];
283 assert!(link_def.auth.is_none());
284 }
285
286 #[test]
287 fn test_mixed_link_auth_configs() {
288 let yaml = r#"
289entities:
290 - singular: order
291 plural: orders
292 - singular: invoice
293 plural: invoices
294 - singular: payment
295 plural: payments
296
297links:
298 - link_type: has_invoice
299 source_type: order
300 target_type: invoice
301 forward_route_name: invoices
302 reverse_route_name: order
303 auth:
304 list: authenticated
305 create: service_only
306 delete: admin_only
307
308 - link_type: payment
309 source_type: invoice
310 target_type: payment
311 forward_route_name: payments
312 reverse_route_name: invoice
313"#;
314
315 let config = LinksConfig::from_yaml_str(yaml).unwrap();
316 assert_eq!(config.links.len(), 2);
317
318 assert!(config.links[0].auth.is_some());
320 let auth1 = config.links[0].auth.as_ref().unwrap();
321 assert_eq!(auth1.create, "service_only");
322
323 assert!(config.links[1].auth.is_none());
325 }
326}