Skip to main content

reinhardt_rest/metadata/
dependencies.rs

1//! Field dependencies and conditional requirements for schema validation
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Represents a field dependency relationship
7///
8/// # Examples
9///
10/// ```
11/// use reinhardt_rest::metadata::{FieldDependency, DependencyType};
12///
13/// // Field 'country' requires 'address' to also be present
14/// let dep = FieldDependency::new(
15///     "country",
16///     DependencyType::Requires { fields: vec!["address".to_string()] }
17/// );
18/// ```
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct FieldDependency {
21	/// The field that has the dependency
22	pub field_name: String,
23	/// The type of dependency
24	pub dependency_type: DependencyType,
25}
26
27impl FieldDependency {
28	/// Creates a new field dependency
29	///
30	/// # Examples
31	///
32	/// ```
33	/// use reinhardt_rest::metadata::{FieldDependency, DependencyType};
34	///
35	/// let dep = FieldDependency::new(
36	///     "zip_code",
37	///     DependencyType::Requires { fields: vec!["city".to_string(), "state".to_string()] }
38	/// );
39	/// assert_eq!(dep.field_name, "zip_code");
40	/// ```
41	pub fn new(field_name: impl Into<String>, dependency_type: DependencyType) -> Self {
42		Self {
43			field_name: field_name.into(),
44			dependency_type,
45		}
46	}
47
48	/// Creates a dependency that requires other fields
49	///
50	/// # Examples
51	///
52	/// ```
53	/// use reinhardt_rest::metadata::FieldDependency;
54	///
55	/// let dep = FieldDependency::requires("country", vec!["address"]);
56	/// ```
57	pub fn requires(
58		field_name: impl Into<String>,
59		required_fields: Vec<impl Into<String>>,
60	) -> Self {
61		Self::new(
62			field_name,
63			DependencyType::Requires {
64				fields: required_fields.into_iter().map(|f| f.into()).collect(),
65			},
66		)
67	}
68
69	/// Creates a dependency where only one of the fields is allowed
70	///
71	/// # Examples
72	///
73	/// ```
74	/// use reinhardt_rest::metadata::FieldDependency;
75	///
76	/// let dep = FieldDependency::one_of("payment_method", vec!["credit_card", "paypal"]);
77	/// ```
78	pub fn one_of(field_name: impl Into<String>, fields: Vec<impl Into<String>>) -> Self {
79		Self::new(
80			field_name,
81			DependencyType::OneOf {
82				fields: fields.into_iter().map(|f| f.into()).collect(),
83			},
84		)
85	}
86
87	/// Creates a dependency where all fields must be present together
88	///
89	/// # Examples
90	///
91	/// ```
92	/// use reinhardt_rest::metadata::FieldDependency;
93	///
94	/// let dep = FieldDependency::all_of("shipping", vec!["address", "city", "zip_code"]);
95	/// ```
96	pub fn all_of(field_name: impl Into<String>, fields: Vec<impl Into<String>>) -> Self {
97		Self::new(
98			field_name,
99			DependencyType::AllOf {
100				fields: fields.into_iter().map(|f| f.into()).collect(),
101			},
102		)
103	}
104
105	/// Creates a conditional dependency based on field value
106	///
107	/// # Examples
108	///
109	/// ```
110	/// use reinhardt_rest::metadata::FieldDependency;
111	///
112	/// // If 'shipping_method' is 'express', then 'express_fee' is required
113	/// let dep = FieldDependency::conditional(
114	///     "shipping_method",
115	///     "express",
116	///     vec!["express_fee"]
117	/// );
118	/// ```
119	pub fn conditional(
120		field_name: impl Into<String>,
121		condition_value: impl Into<String>,
122		required_fields: Vec<impl Into<String>>,
123	) -> Self {
124		Self::new(
125			field_name,
126			DependencyType::Conditional {
127				value: condition_value.into(),
128				requires: required_fields.into_iter().map(|f| f.into()).collect(),
129			},
130		)
131	}
132}
133
134/// Types of field dependencies
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136#[serde(tag = "type", rename_all = "snake_case")]
137pub enum DependencyType {
138	/// Requires all specified fields to be present
139	Requires {
140		/// List of required field names
141		fields: Vec<String>,
142	},
143	/// Only one of the specified fields can be present
144	OneOf {
145		/// List of field names where only one can be present
146		fields: Vec<String>,
147	},
148	/// All specified fields must be present together
149	AllOf {
150		/// List of field names that must all be present
151		fields: Vec<String>,
152	},
153	/// Conditional requirement based on field value
154	Conditional {
155		/// The value that triggers the dependency
156		value: String,
157		/// Fields required when the condition is met
158		requires: Vec<String>,
159	},
160}
161
162/// Manages field dependencies for a schema
163///
164/// # Examples
165///
166/// ```
167/// use reinhardt_rest::metadata::{DependencyManager, FieldDependency};
168///
169/// let mut manager = DependencyManager::new();
170/// manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
171/// ```
172#[derive(Debug, Clone, Default)]
173pub struct DependencyManager {
174	dependencies: Vec<FieldDependency>,
175}
176
177impl DependencyManager {
178	/// Creates a new dependency manager
179	///
180	/// # Examples
181	///
182	/// ```
183	/// use reinhardt_rest::metadata::DependencyManager;
184	///
185	/// let manager = DependencyManager::new();
186	/// ```
187	pub fn new() -> Self {
188		Self {
189			dependencies: Vec::new(),
190		}
191	}
192
193	/// Adds a field dependency
194	///
195	/// # Examples
196	///
197	/// ```
198	/// use reinhardt_rest::metadata::{DependencyManager, FieldDependency};
199	///
200	/// let mut manager = DependencyManager::new();
201	/// manager.add_dependency(FieldDependency::requires("zip_code", vec!["city"]));
202	/// ```
203	pub fn add_dependency(&mut self, dependency: FieldDependency) {
204		self.dependencies.push(dependency);
205	}
206
207	/// Gets all dependencies
208	///
209	/// # Examples
210	///
211	/// ```
212	/// use reinhardt_rest::metadata::{DependencyManager, FieldDependency};
213	///
214	/// let mut manager = DependencyManager::new();
215	/// manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
216	/// assert_eq!(manager.get_dependencies().len(), 1);
217	/// ```
218	pub fn get_dependencies(&self) -> &[FieldDependency] {
219		&self.dependencies
220	}
221
222	/// Gets dependencies for a specific field
223	///
224	/// # Examples
225	///
226	/// ```
227	/// use reinhardt_rest::metadata::{DependencyManager, FieldDependency};
228	///
229	/// let mut manager = DependencyManager::new();
230	/// manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
231	/// manager.add_dependency(FieldDependency::requires("city", vec!["address"]));
232	///
233	/// let country_deps = manager.get_field_dependencies("country");
234	/// assert_eq!(country_deps.len(), 1);
235	/// ```
236	pub fn get_field_dependencies(&self, field_name: &str) -> Vec<&FieldDependency> {
237		self.dependencies
238			.iter()
239			.filter(|dep| dep.field_name == field_name)
240			.collect()
241	}
242
243	/// Converts dependencies to OpenAPI schema format
244	///
245	/// # Examples
246	///
247	/// ```
248	/// use reinhardt_rest::metadata::{DependencyManager, FieldDependency};
249	///
250	/// let mut manager = DependencyManager::new();
251	/// manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
252	///
253	/// let openapi = manager.to_openapi_dependencies();
254	/// assert!(openapi.contains_key("country"));
255	/// ```
256	pub fn to_openapi_dependencies(&self) -> HashMap<String, serde_json::Value> {
257		let mut result = HashMap::new();
258
259		for dep in &self.dependencies {
260			match &dep.dependency_type {
261				DependencyType::Requires { fields } => {
262					result.insert(dep.field_name.clone(), serde_json::json!(fields));
263				}
264				DependencyType::OneOf { fields } => {
265					result.insert(
266						dep.field_name.clone(),
267						serde_json::json!({
268							"oneOf": fields.iter().map(|f| {
269								serde_json::json!({
270									"required": [f]
271								})
272							}).collect::<Vec<_>>()
273						}),
274					);
275				}
276				DependencyType::AllOf { fields } => {
277					result.insert(
278						dep.field_name.clone(),
279						serde_json::json!({
280							"allOf": fields.iter().map(|f| {
281								serde_json::json!({
282									"required": [f]
283								})
284							}).collect::<Vec<_>>()
285						}),
286					);
287				}
288				DependencyType::Conditional { value, requires } => {
289					result.insert(
290						dep.field_name.clone(),
291						serde_json::json!({
292							"if": {
293								"properties": {
294									dep.field_name.clone(): {
295										"const": value
296									}
297								}
298							},
299							"then": {
300								"required": requires
301							}
302						}),
303					);
304				}
305			}
306		}
307
308		result
309	}
310
311	/// Validates that field dependencies are satisfied
312	///
313	/// # Examples
314	///
315	/// ```
316	/// use reinhardt_rest::metadata::{DependencyManager, FieldDependency};
317	/// use std::collections::HashSet;
318	///
319	/// let mut manager = DependencyManager::new();
320	/// manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
321	///
322	/// let mut present_fields = HashSet::new();
323	/// present_fields.insert("country".to_string());
324	/// present_fields.insert("address".to_string());
325	///
326	/// let errors = manager.validate_dependencies(&present_fields);
327	/// assert!(errors.is_empty());
328	/// ```
329	pub fn validate_dependencies(
330		&self,
331		present_fields: &std::collections::HashSet<String>,
332	) -> Vec<String> {
333		let mut errors = Vec::new();
334
335		for dep in &self.dependencies {
336			if !present_fields.contains(&dep.field_name) {
337				continue;
338			}
339
340			match &dep.dependency_type {
341				DependencyType::Requires { fields } => {
342					for required_field in fields {
343						if !present_fields.contains(required_field) {
344							errors.push(format!(
345								"Field '{}' requires field '{}' to be present",
346								dep.field_name, required_field
347							));
348						}
349					}
350				}
351				DependencyType::OneOf { fields } => {
352					let count = fields
353						.iter()
354						.filter(|f| present_fields.contains(*f))
355						.count();
356					if count != 1 {
357						errors.push(format!(
358							"Field '{}' requires exactly one of: {:?}",
359							dep.field_name, fields
360						));
361					}
362				}
363				DependencyType::AllOf { fields } => {
364					let missing: Vec<_> = fields
365						.iter()
366						.filter(|f| !present_fields.contains(*f))
367						.collect();
368					if !missing.is_empty() {
369						errors.push(format!(
370							"Field '{}' requires all of: {:?}, missing: {:?}",
371							dep.field_name, fields, missing
372						));
373					}
374				}
375				DependencyType::Conditional { requires, .. } => {
376					// For conditional dependencies, we only validate if the condition is met
377					// This would require actual field values, not just presence
378					for required_field in requires {
379						if !present_fields.contains(required_field) {
380							errors.push(format!(
381								"Field '{}' conditionally requires field '{}'",
382								dep.field_name, required_field
383							));
384						}
385					}
386				}
387			}
388		}
389
390		errors
391	}
392}
393
394#[cfg(test)]
395mod tests {
396	use super::*;
397	use rstest::rstest;
398	use std::collections::HashSet;
399
400	#[rstest]
401	fn test_create_requires_dependency() {
402		let dep = FieldDependency::requires("country", vec!["address"]);
403		assert_eq!(dep.field_name, "country");
404		match dep.dependency_type {
405			DependencyType::Requires { fields } => {
406				assert_eq!(fields.len(), 1);
407				assert_eq!(fields[0], "address");
408			}
409			_ => panic!("Wrong dependency type"),
410		}
411	}
412
413	#[rstest]
414	fn test_create_one_of_dependency() {
415		let dep = FieldDependency::one_of("payment", vec!["credit_card", "paypal"]);
416		match dep.dependency_type {
417			DependencyType::OneOf { fields } => {
418				assert_eq!(fields.len(), 2);
419				assert!(fields.contains(&"credit_card".to_string()));
420				assert!(fields.contains(&"paypal".to_string()));
421			}
422			_ => panic!("Wrong dependency type"),
423		}
424	}
425
426	#[rstest]
427	fn test_create_all_of_dependency() {
428		let dep = FieldDependency::all_of("shipping", vec!["address", "city", "zip"]);
429		match dep.dependency_type {
430			DependencyType::AllOf { fields } => {
431				assert_eq!(fields.len(), 3);
432			}
433			_ => panic!("Wrong dependency type"),
434		}
435	}
436
437	#[rstest]
438	fn test_create_conditional_dependency() {
439		let dep = FieldDependency::conditional("shipping_method", "express", vec!["express_fee"]);
440		match dep.dependency_type {
441			DependencyType::Conditional { value, requires } => {
442				assert_eq!(value, "express");
443				assert_eq!(requires.len(), 1);
444				assert_eq!(requires[0], "express_fee");
445			}
446			_ => panic!("Wrong dependency type"),
447		}
448	}
449
450	#[rstest]
451	fn test_dependency_manager_add_and_get() {
452		let mut manager = DependencyManager::new();
453		manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
454		manager.add_dependency(FieldDependency::requires("zip_code", vec!["city"]));
455
456		assert_eq!(manager.get_dependencies().len(), 2);
457	}
458
459	#[rstest]
460	fn test_get_field_dependencies() {
461		let mut manager = DependencyManager::new();
462		manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
463		manager.add_dependency(FieldDependency::requires("city", vec!["address"]));
464		manager.add_dependency(FieldDependency::requires("country", vec!["phone"]));
465
466		let country_deps = manager.get_field_dependencies("country");
467		assert_eq!(country_deps.len(), 2);
468
469		let city_deps = manager.get_field_dependencies("city");
470		assert_eq!(city_deps.len(), 1);
471	}
472
473	#[rstest]
474	fn test_to_openapi_dependencies_requires() {
475		let mut manager = DependencyManager::new();
476		manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
477
478		let openapi = manager.to_openapi_dependencies();
479		assert!(openapi.contains_key("country"));
480
481		let country_deps = &openapi["country"];
482		assert!(country_deps.is_array());
483		assert_eq!(country_deps.as_array().unwrap().len(), 1);
484	}
485
486	#[rstest]
487	fn test_validate_dependencies_success() {
488		let mut manager = DependencyManager::new();
489		manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
490
491		let mut present_fields = HashSet::new();
492		present_fields.insert("country".to_string());
493		present_fields.insert("address".to_string());
494
495		let errors = manager.validate_dependencies(&present_fields);
496		assert!(errors.is_empty());
497	}
498
499	#[rstest]
500	fn test_validate_dependencies_missing_required() {
501		let mut manager = DependencyManager::new();
502		manager.add_dependency(FieldDependency::requires("country", vec!["address"]));
503
504		let mut present_fields = HashSet::new();
505		present_fields.insert("country".to_string());
506		// address is missing
507
508		let errors = manager.validate_dependencies(&present_fields);
509		assert_eq!(errors.len(), 1);
510		assert!(errors[0].contains("requires"));
511	}
512
513	#[rstest]
514	fn test_validate_one_of_success() {
515		let mut manager = DependencyManager::new();
516		manager.add_dependency(FieldDependency::one_of(
517			"payment",
518			vec!["credit_card", "paypal"],
519		));
520
521		let mut present_fields = HashSet::new();
522		present_fields.insert("payment".to_string());
523		present_fields.insert("credit_card".to_string());
524
525		let errors = manager.validate_dependencies(&present_fields);
526		assert!(errors.is_empty());
527	}
528
529	#[rstest]
530	fn test_validate_one_of_multiple_present() {
531		let mut manager = DependencyManager::new();
532		manager.add_dependency(FieldDependency::one_of(
533			"payment",
534			vec!["credit_card", "paypal"],
535		));
536
537		let mut present_fields = HashSet::new();
538		present_fields.insert("payment".to_string());
539		present_fields.insert("credit_card".to_string());
540		present_fields.insert("paypal".to_string());
541
542		let errors = manager.validate_dependencies(&present_fields);
543		assert_eq!(errors.len(), 1);
544		assert!(errors[0].contains("exactly one"));
545	}
546
547	#[rstest]
548	fn test_validate_all_of_success() {
549		let mut manager = DependencyManager::new();
550		manager.add_dependency(FieldDependency::all_of(
551			"shipping",
552			vec!["address", "city", "zip"],
553		));
554
555		let mut present_fields = HashSet::new();
556		present_fields.insert("shipping".to_string());
557		present_fields.insert("address".to_string());
558		present_fields.insert("city".to_string());
559		present_fields.insert("zip".to_string());
560
561		let errors = manager.validate_dependencies(&present_fields);
562		assert!(errors.is_empty());
563	}
564
565	#[rstest]
566	fn test_validate_all_of_missing_some() {
567		let mut manager = DependencyManager::new();
568		manager.add_dependency(FieldDependency::all_of(
569			"shipping",
570			vec!["address", "city", "zip"],
571		));
572
573		let mut present_fields = HashSet::new();
574		present_fields.insert("shipping".to_string());
575		present_fields.insert("address".to_string());
576		// city and zip are missing
577
578		let errors = manager.validate_dependencies(&present_fields);
579		assert_eq!(errors.len(), 1);
580		assert!(errors[0].contains("requires all of"));
581	}
582
583	#[rstest]
584	fn test_dependency_type_serialization() {
585		let dep = FieldDependency::requires("country", vec!["address"]);
586		let json = serde_json::to_string(&dep).unwrap();
587		let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
588
589		assert_eq!(
590			parsed["field_name"], "country",
591			"Metadata field name mismatch. Expected 'country', got: {:?}",
592			parsed["field_name"]
593		);
594		assert_eq!(
595			parsed["dependency_type"]["type"], "requires",
596			"Dependency type mismatch. Expected 'requires', got: {:?}",
597			parsed["dependency_type"]["type"]
598		);
599		assert_eq!(
600			parsed["dependency_type"]["fields"][0], "address",
601			"Required field mismatch. Expected 'address', got: {:?}",
602			parsed["dependency_type"]["fields"][0]
603		);
604	}
605
606	#[rstest]
607	fn test_conditional_dependency_serialization() {
608		let dep = FieldDependency::conditional("shipping_method", "express", vec!["express_fee"]);
609		let json = serde_json::to_string(&dep).unwrap();
610		let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
611
612		assert_eq!(
613			parsed["field_name"], "shipping_method",
614			"Metadata field name mismatch. Expected 'shipping_method', got: {:?}",
615			parsed["field_name"]
616		);
617		assert_eq!(
618			parsed["dependency_type"]["type"], "conditional",
619			"Dependency type mismatch. Expected 'conditional', got: {:?}",
620			parsed["dependency_type"]["type"]
621		);
622		assert_eq!(
623			parsed["dependency_type"]["value"], "express",
624			"Conditional value mismatch. Expected 'express', got: {:?}",
625			parsed["dependency_type"]["value"]
626		);
627		assert_eq!(
628			parsed["dependency_type"]["requires"][0], "express_fee",
629			"Required field mismatch. Expected 'express_fee', got: {:?}",
630			parsed["dependency_type"]["requires"][0]
631		);
632	}
633}