rustrails-support 0.1.1

Core utilities (ActiveSupport equivalent)
Documentation
/// Marker trait for concerns (mixin traits).
pub trait Concern: Send + Sync {}

/// A registry that tracks which concerns have been included.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ConcernRegistry {
    concerns: Vec<String>,
}

impl ConcernRegistry {
    /// Creates an empty concern registry.
    #[must_use]
    pub fn new() -> Self {
        Self {
            concerns: Vec::new(),
        }
    }

    /// Registers a concern name if it has not already been recorded.
    pub fn register(&mut self, name: impl Into<String>) {
        let name = name.into();
        if !self.includes(&name) {
            self.concerns.push(name);
        }
    }

    /// Returns `true` when the registry contains the provided concern name.
    #[must_use]
    pub fn includes(&self, name: &str) -> bool {
        self.concerns.iter().any(|registered| registered == name)
    }

    /// Returns all registered concern names in registration order.
    #[must_use]
    pub fn all(&self) -> &[String] {
        &self.concerns
    }
}

#[cfg(test)]
mod tests {
    use super::{Concern, ConcernRegistry};

    struct Auditable;
    impl Concern for Auditable {}

    #[test]
    fn concern_registry_starts_empty() {
        let registry = ConcernRegistry::new();

        assert!(registry.all().is_empty());
        assert!(!registry.includes("Auditable"));
    }

    #[test]
    fn concern_registry_registers_names() {
        let mut registry = ConcernRegistry::new();
        registry.register("Auditable");

        assert!(registry.includes("Auditable"));
        assert_eq!(registry.all(), &[String::from("Auditable")]);
    }

    #[test]
    fn concern_registry_preserves_registration_order() {
        let mut registry = ConcernRegistry::new();
        registry.register("Auditable");
        registry.register("Trackable");

        assert_eq!(
            registry.all(),
            &[String::from("Auditable"), String::from("Trackable")]
        );
    }

    #[test]
    fn concern_registry_ignores_duplicates() {
        let mut registry = ConcernRegistry::new();
        registry.register("Auditable");
        registry.register("Auditable");

        assert_eq!(registry.all(), &[String::from("Auditable")]);
    }

    #[test]
    fn concern_trait_can_be_implemented_by_marker_types() {
        fn assert_concern<T: Concern>() {}

        assert_concern::<Auditable>();
    }
    #[derive(Debug)]
    struct Trackable;
    impl Concern for Trackable {}

    #[derive(Debug)]
    struct Publishable;
    impl Concern for Publishable {}

    fn short_type_name<T>() -> String {
        let full = std::any::type_name::<T>();
        full.rsplit("::")
            .next()
            .map_or_else(|| full.to_string(), str::to_string)
    }

    #[test]
    fn concern_registry_default_matches_new() {
        assert_eq!(ConcernRegistry::default(), ConcernRegistry::new());
    }

    #[test]
    fn concern_registry_clone_preserves_registered_names() {
        let mut registry = ConcernRegistry::new();
        registry.register("Auditable");
        registry.register("Trackable");

        let clone = registry.clone();

        assert_eq!(clone, registry);
        assert_eq!(
            clone.all(),
            &[String::from("Auditable"), String::from("Trackable")]
        );
    }

    #[test]
    fn concern_registry_instances_are_independent() {
        let mut first = ConcernRegistry::new();
        let mut second = ConcernRegistry::new();

        first.register("Auditable");
        second.register("Trackable");

        assert!(first.includes("Auditable"));
        assert!(!first.includes("Trackable"));
        assert!(second.includes("Trackable"));
        assert!(!second.includes("Auditable"));
    }

    #[test]
    fn concern_registry_registers_owned_strings() {
        let mut registry = ConcernRegistry::new();
        registry.register(String::from("Publishable"));

        assert!(registry.includes("Publishable"));
        assert_eq!(registry.all(), &[String::from("Publishable")]);
    }

    #[test]
    fn concern_registry_includes_is_case_sensitive() {
        let mut registry = ConcernRegistry::new();
        registry.register("Auditable");

        assert!(registry.includes("Auditable"));
        assert!(!registry.includes("auditable"));
    }

    #[test]
    fn concern_registry_supports_multiple_marker_type_names() {
        let mut registry = ConcernRegistry::new();
        let names = [
            short_type_name::<Auditable>(),
            short_type_name::<Trackable>(),
            short_type_name::<Publishable>(),
        ];

        for name in &names {
            registry.register(name.clone());
        }

        for name in &names {
            assert!(registry.includes(name));
        }
        assert_eq!(registry.all(), &names);
    }

    #[test]
    fn concern_registry_registers_empty_names_once() {
        let mut registry = ConcernRegistry::new();
        registry.register("");
        registry.register(String::new());

        assert!(registry.includes(""));
        assert_eq!(registry.all(), &[String::new()]);
    }

    #[test]
    fn concern_registry_preserves_first_occurrence_across_owned_and_borrowed_duplicates() {
        let mut registry = ConcernRegistry::new();
        let auditable = String::from("Auditable");
        let trackable = String::from("Trackable");

        registry.register(auditable.clone());
        registry.register("Trackable");
        registry.register(auditable);
        registry.register(trackable);

        assert_eq!(
            registry.all(),
            &[String::from("Auditable"), String::from("Trackable")]
        );
    }

    #[test]
    fn concern_registry_clone_remains_independent_after_additional_registration() {
        let mut original = ConcernRegistry::new();
        original.register("Auditable");

        let mut clone = original.clone();
        clone.register("Trackable");

        assert_eq!(original.all(), &[String::from("Auditable")]);
        assert_eq!(
            clone.all(),
            &[String::from("Auditable"), String::from("Trackable")]
        );
    }

    #[test]
    fn concern_registry_treats_empty_and_whitespace_names_as_distinct() {
        let mut registry = ConcernRegistry::new();
        registry.register("");
        registry.register(" ");

        assert!(registry.includes(""));
        assert!(registry.includes(" "));
        assert_eq!(registry.all(), &[String::new(), String::from(" ")]);
    }

    #[test]
    fn concern_registry_preserves_first_occurrence_across_interleaved_duplicates() {
        let mut registry = ConcernRegistry::new();
        registry.register("Auditable");
        registry.register("Trackable");
        registry.register("Auditable");
        registry.register("Publishable");
        registry.register("Trackable");

        assert_eq!(
            registry.all(),
            &[
                String::from("Auditable"),
                String::from("Trackable"),
                String::from("Publishable"),
            ]
        );
    }

    #[test]
    fn multiple_marker_types_can_implement_concern_trait() {
        fn assert_concern<T: Concern>() {}

        assert_concern::<Auditable>();
        assert_concern::<Trackable>();
        assert_concern::<Publishable>();
    }
}