use crate::{ ServiceCardinality, ServiceCollection, ServiceDependency, ServiceDescriptor, ServiceLifetime, Type, }; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; fn expand_type(t: &Type) -> String { let (name, key) = Type::deconstruct(t); match key { Some(val) => format!("'{}' with the key '{}'", name, val), _ => format!("'{}'", name), } } #[derive(Clone, Debug)] struct ValidationResult { message: String, } impl ValidationResult { fn fail>(message: T) -> Self { Self { message: String::from(message.as_ref()), } } } /// Represents an validation error. #[derive(Clone, Debug)] pub struct ValidationError { message: String, results: Vec, } impl ValidationError { fn fail(results: Vec) -> Self { Self { message: if results.is_empty() { String::from("Validation failed.") } else if results.len() == 1 { results[0].message.clone() } else { String::from("One or more validation errors occurred.") }, results, } } } impl Display for ValidationError { fn fmt(&self, formatter: &mut Formatter) -> Result<(), std::fmt::Error> { write!(formatter, "{}", self.message)?; if self.results.len() > 1 { for (i, result) in self.results.iter().enumerate() { write!(formatter, "\n [{}] {}", i + 1, result.message)?; } } Ok(()) } } impl std::error::Error for ValidationError { fn description(&self) -> &str { "validation error" } } trait ValidationRule<'a> { fn evaluate(&mut self, descriptor: &'a ServiceDescriptor, results: &mut Vec); } struct MissingRequiredType<'a> { lookup: &'a HashMap<&'a Type, Vec<&'a ServiceDescriptor>>, } impl<'a> MissingRequiredType<'a> { fn new(lookup: &'a HashMap<&'a Type, Vec<&'a ServiceDescriptor>>) -> Self { Self { lookup } } } impl<'a> ValidationRule<'a> for MissingRequiredType<'a> { fn evaluate(&mut self, descriptor: &'a ServiceDescriptor, results: &mut Vec) { for dependency in descriptor.dependencies() { if dependency.cardinality() == ServiceCardinality::ExactlyOne && !self.lookup.contains_key(dependency.injected_type()) { results.push(ValidationResult::fail(format!( "Service '{}' requires dependent service {}, which has not be registered", descriptor.implementation_type().name(), expand_type(dependency.injected_type()) ))); } } } } struct CircularDependency<'a> { lookup: &'a HashMap<&'a Type, Vec<&'a ServiceDescriptor>>, visited: HashSet<&'a Type>, queue: Vec<&'a ServiceDependency>, } impl<'a> CircularDependency<'a> { fn new(lookup: &'a HashMap<&'a Type, Vec<&'a ServiceDescriptor>>) -> Self { Self { lookup, visited: HashSet::new(), queue: Vec::new(), } } fn check_dependency_graph( &mut self, root: &'a ServiceDescriptor, dependency: &'a ServiceDependency, results: &mut Vec, ) { self.queue.clear(); self.queue.push(dependency); while let Some(current) = self.queue.pop() { if let Some(descriptors) = self.lookup.get(current.injected_type()) { for descriptor in descriptors { if self.visited.insert(descriptor.service_type()) { self.queue.extend(descriptor.dependencies()); } if descriptor.service_type() == root.service_type() { results.push(ValidationResult::fail(format!( "A circular dependency was detected for service {} on service '{}'", expand_type(descriptor.service_type()), root.implementation_type().name() ))); } } } } } } impl<'a> ValidationRule<'a> for CircularDependency<'a> { fn evaluate(&mut self, descriptor: &'a ServiceDescriptor, results: &mut Vec) { for dependency in descriptor.dependencies() { self.visited.clear(); self.visited.insert(descriptor.service_type()); self.check_dependency_graph(descriptor, dependency, results); } } } struct SingletonDependsOnScoped<'a> { lookup: &'a HashMap<&'a Type, Vec<&'a ServiceDescriptor>>, visited: HashSet<&'a Type>, queue: Vec<&'a ServiceDescriptor>, } impl<'a> SingletonDependsOnScoped<'a> { fn new(lookup: &'a HashMap<&'a Type, Vec<&'a ServiceDescriptor>>) -> Self { Self { lookup, visited: HashSet::new(), queue: Vec::new(), } } } impl<'a> ValidationRule<'a> for SingletonDependsOnScoped<'a> { fn evaluate(&mut self, descriptor: &'a ServiceDescriptor, results: &mut Vec) { if descriptor.lifetime() != ServiceLifetime::Singleton { return; } let mut level = ""; self.visited.clear(); self.queue.clear(); self.queue.push(descriptor); while let Some(current) = self.queue.pop() { if !self.visited.insert(current.service_type()) { continue; } for dependency in current.dependencies() { if let Some(descriptors) = self.lookup.get(dependency.injected_type()) { for next in descriptors { self.queue.push(next); if next.lifetime() == ServiceLifetime::Scoped { results.push(ValidationResult::fail(format!( "The service {} has a singleton lifetime, \ but its {}dependency '{}' has a scoped lifetime", expand_type(descriptor.implementation_type()), level, next.service_type().name() ))); } } } } level = "transitive "; } } } /// Validates the specified [`ServiceCollection`](crate::ServiceCollection). /// /// # Arguments /// /// * `services` - The [`ServiceCollection`](crate::ServiceCollection) to validate pub fn validate(services: &ServiceCollection) -> Result<(), ValidationError> { let mut lookup = HashMap::with_capacity(services.len()); for item in services.iter() { let key = item.service_type(); let descriptors = lookup.entry(key).or_insert_with(Vec::new); descriptors.push(item); } let mut results = Vec::new(); let mut missing_type = MissingRequiredType::new(&lookup); let mut circular_dep = CircularDependency::new(&lookup); let mut scoped_in_singleton = SingletonDependsOnScoped::new(&lookup); let mut rules: Vec<&mut dyn ValidationRule> = vec![ &mut missing_type, &mut circular_dep, &mut scoped_in_singleton, ]; for descriptor in services { for rule in rules.iter_mut() { rule.evaluate(descriptor, &mut results); } } if results.is_empty() { Ok(()) } else { Err(ValidationError::fail(results)) } } #[cfg(test)] mod tests { use super::*; use crate::{test::*, *}; #[test] fn validate_should_report_missing_required_type() { // arrange let mut services = ServiceCollection::new(); services.add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(OtherTestServiceImpl::new( sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert_eq!( &result.err().unwrap().to_string(), "Service 'di::test::OtherTestServiceImpl' requires dependent service \ 'dyn di::test::TestService', which has not be registered" ); } #[test] fn validate_should_report_missing_required_keyed_type() { // arrange let mut services = ServiceCollection::new(); services .add( singleton_as_self::() .depends_on(exactly_one_with_key::()) .depends_on(zero_or_one_with_key::()) .from(|sp| { Ref::new(CatInTheHat::new( sp.get_required_by_key::(), sp.get_by_key::(), )) }), ) .add( transient_with_key::() .from(|_| Ref::new(Thing2::default())), ); // act let result = validate(&services); // assert assert_eq!( &result.err().unwrap().to_string(), "Service 'di::test::CatInTheHat' requires dependent service \ 'dyn di::test::Thing' with the key 'di::test::key::Thing1', which has not be registered" ); } #[test] fn validate_should_ignore_missing_optional_type() { // arrange let mut services = ServiceCollection::new(); services.add( singleton::() .depends_on(zero_or_one::()) .from(|sp| Ref::new(TestOptionalDepImpl::new(sp.get::()))), ); // act let result = validate(&services); // assert assert!(result.is_ok()); } #[test] fn validate_should_ignore_missing_optional_keyed_type() { // arrange let mut services = ServiceCollection::new(); services .add( singleton_as_self::() .depends_on(exactly_one_with_key::()) .depends_on(zero_or_one_with_key::()) .from(|sp| { Ref::new(CatInTheHat::new( sp.get_required_by_key::(), sp.get_by_key::(), )) }), ) .add( transient_with_key::() .from(|_| Ref::new(Thing1::default())), ); // act let result = validate(&services); // assert assert!(result.is_ok()); } #[test] fn validate_should_report_circular_dependency() { // arrange let mut services = ServiceCollection::new(); services.add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(TestCircularDepImpl::new( sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert_eq!( &result.err().unwrap().to_string(), "A circular dependency was detected for service \ 'dyn di::test::TestService' on service 'di::test::TestCircularDepImpl'" ); } #[test] fn validate_should_report_multiple_issues() { // arrange let mut services = ServiceCollection::new(); services .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(TestAllKindOfProblems::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(OtherTestServiceImpl::new( sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert_eq!( &result.err().unwrap().to_string(), "One or more validation errors occurred.\n \ [1] Service 'di::test::TestAllKindOfProblems' requires dependent service 'dyn di::test::AnotherTestService', which has not be registered\n \ [2] A circular dependency was detected for service 'dyn di::test::TestService' on service 'di::test::TestAllKindOfProblems'\n \ [3] A circular dependency was detected for service 'dyn di::test::OtherTestService' on service 'di::test::OtherTestServiceImpl'"); } #[test] fn validate_should_report_scoped_service_in_singleton() { // arrange let mut services = ServiceCollection::new(); services .add( scoped::() .from(|_| Ref::new(TestServiceImpl::default())), ) .add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(OtherTestServiceImpl::new( sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert_eq!( &result.err().unwrap().to_string(), "The service 'di::test::OtherTestServiceImpl' has a singleton lifetime, \ but its dependency 'dyn di::test::TestService' has a scoped lifetime" ); } #[test] fn validate_should_report_transitive_scoped_service_in_singleton() { // arrange let mut services = ServiceCollection::new(); services .add( scoped::() .from(|_| Ref::new(TestServiceImpl::default())), ) .add( transient::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(OtherTestServiceImpl::new( sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(AnotherTestServiceImpl::new( sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert_eq!( &result.err().unwrap().to_string(), "The service 'di::test::AnotherTestServiceImpl' has a singleton lifetime, \ but its transitive dependency 'dyn di::test::TestService' has a scoped lifetime" ); } #[test] fn validate_should_not_report_circular_dependency_when_visited_multiple_times() { // arrange let mut services = ServiceCollection::new(); services .add( singleton::().from(|_sp| Ref::new(ServiceMImpl)), ) .add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceBImpl::new(sp.get_required::())) }), ) .add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceCImpl::new(sp.get_required::())) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceAImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceYImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceXImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceZImpl::new( sp.get_required::(), sp.get_required::(), sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert!(result.is_ok()); } #[test] fn validate_should_report_circular_dependency_in_complex_dependency_tree() { // arrange let mut services = ServiceCollection::new(); services .add( singleton::().from(|_sp| Ref::new(ServiceMImpl)), ) .add( singleton::() .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceBImpl::new(sp.get_required::())) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceCWithCircleRefToXImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceAImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceYImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceXImpl::new( sp.get_required::(), sp.get_required::(), )) }), ) .add( singleton::() .depends_on(exactly_one::()) .depends_on(exactly_one::()) .depends_on(exactly_one::()) .from(|sp| { Ref::new(ServiceZImpl::new( sp.get_required::(), sp.get_required::(), sp.get_required::(), )) }), ); // act let result = validate(&services); // assert assert!(result.is_err()); } }