Skip to main content

rustrails_support/
concern.rs

1/// Marker trait for concerns (mixin traits).
2pub trait Concern: Send + Sync {}
3
4/// A registry that tracks which concerns have been included.
5#[derive(Debug, Default, Clone, PartialEq, Eq)]
6pub struct ConcernRegistry {
7    concerns: Vec<String>,
8}
9
10impl ConcernRegistry {
11    /// Creates an empty concern registry.
12    #[must_use]
13    pub fn new() -> Self {
14        Self {
15            concerns: Vec::new(),
16        }
17    }
18
19    /// Registers a concern name if it has not already been recorded.
20    pub fn register(&mut self, name: impl Into<String>) {
21        let name = name.into();
22        if !self.includes(&name) {
23            self.concerns.push(name);
24        }
25    }
26
27    /// Returns `true` when the registry contains the provided concern name.
28    #[must_use]
29    pub fn includes(&self, name: &str) -> bool {
30        self.concerns.iter().any(|registered| registered == name)
31    }
32
33    /// Returns all registered concern names in registration order.
34    #[must_use]
35    pub fn all(&self) -> &[String] {
36        &self.concerns
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::{Concern, ConcernRegistry};
43
44    struct Auditable;
45    impl Concern for Auditable {}
46
47    #[test]
48    fn concern_registry_starts_empty() {
49        let registry = ConcernRegistry::new();
50
51        assert!(registry.all().is_empty());
52        assert!(!registry.includes("Auditable"));
53    }
54
55    #[test]
56    fn concern_registry_registers_names() {
57        let mut registry = ConcernRegistry::new();
58        registry.register("Auditable");
59
60        assert!(registry.includes("Auditable"));
61        assert_eq!(registry.all(), &[String::from("Auditable")]);
62    }
63
64    #[test]
65    fn concern_registry_preserves_registration_order() {
66        let mut registry = ConcernRegistry::new();
67        registry.register("Auditable");
68        registry.register("Trackable");
69
70        assert_eq!(
71            registry.all(),
72            &[String::from("Auditable"), String::from("Trackable")]
73        );
74    }
75
76    #[test]
77    fn concern_registry_ignores_duplicates() {
78        let mut registry = ConcernRegistry::new();
79        registry.register("Auditable");
80        registry.register("Auditable");
81
82        assert_eq!(registry.all(), &[String::from("Auditable")]);
83    }
84
85    #[test]
86    fn concern_trait_can_be_implemented_by_marker_types() {
87        fn assert_concern<T: Concern>() {}
88
89        assert_concern::<Auditable>();
90    }
91    #[derive(Debug)]
92    struct Trackable;
93    impl Concern for Trackable {}
94
95    #[derive(Debug)]
96    struct Publishable;
97    impl Concern for Publishable {}
98
99    fn short_type_name<T>() -> String {
100        let full = std::any::type_name::<T>();
101        full.rsplit("::")
102            .next()
103            .map_or_else(|| full.to_string(), str::to_string)
104    }
105
106    #[test]
107    fn concern_registry_default_matches_new() {
108        assert_eq!(ConcernRegistry::default(), ConcernRegistry::new());
109    }
110
111    #[test]
112    fn concern_registry_clone_preserves_registered_names() {
113        let mut registry = ConcernRegistry::new();
114        registry.register("Auditable");
115        registry.register("Trackable");
116
117        let clone = registry.clone();
118
119        assert_eq!(clone, registry);
120        assert_eq!(
121            clone.all(),
122            &[String::from("Auditable"), String::from("Trackable")]
123        );
124    }
125
126    #[test]
127    fn concern_registry_instances_are_independent() {
128        let mut first = ConcernRegistry::new();
129        let mut second = ConcernRegistry::new();
130
131        first.register("Auditable");
132        second.register("Trackable");
133
134        assert!(first.includes("Auditable"));
135        assert!(!first.includes("Trackable"));
136        assert!(second.includes("Trackable"));
137        assert!(!second.includes("Auditable"));
138    }
139
140    #[test]
141    fn concern_registry_registers_owned_strings() {
142        let mut registry = ConcernRegistry::new();
143        registry.register(String::from("Publishable"));
144
145        assert!(registry.includes("Publishable"));
146        assert_eq!(registry.all(), &[String::from("Publishable")]);
147    }
148
149    #[test]
150    fn concern_registry_includes_is_case_sensitive() {
151        let mut registry = ConcernRegistry::new();
152        registry.register("Auditable");
153
154        assert!(registry.includes("Auditable"));
155        assert!(!registry.includes("auditable"));
156    }
157
158    #[test]
159    fn concern_registry_supports_multiple_marker_type_names() {
160        let mut registry = ConcernRegistry::new();
161        let names = [
162            short_type_name::<Auditable>(),
163            short_type_name::<Trackable>(),
164            short_type_name::<Publishable>(),
165        ];
166
167        for name in &names {
168            registry.register(name.clone());
169        }
170
171        for name in &names {
172            assert!(registry.includes(name));
173        }
174        assert_eq!(registry.all(), &names);
175    }
176
177    #[test]
178    fn concern_registry_registers_empty_names_once() {
179        let mut registry = ConcernRegistry::new();
180        registry.register("");
181        registry.register(String::new());
182
183        assert!(registry.includes(""));
184        assert_eq!(registry.all(), &[String::new()]);
185    }
186
187    #[test]
188    fn concern_registry_preserves_first_occurrence_across_owned_and_borrowed_duplicates() {
189        let mut registry = ConcernRegistry::new();
190        let auditable = String::from("Auditable");
191        let trackable = String::from("Trackable");
192
193        registry.register(auditable.clone());
194        registry.register("Trackable");
195        registry.register(auditable);
196        registry.register(trackable);
197
198        assert_eq!(
199            registry.all(),
200            &[String::from("Auditable"), String::from("Trackable")]
201        );
202    }
203
204    #[test]
205    fn concern_registry_clone_remains_independent_after_additional_registration() {
206        let mut original = ConcernRegistry::new();
207        original.register("Auditable");
208
209        let mut clone = original.clone();
210        clone.register("Trackable");
211
212        assert_eq!(original.all(), &[String::from("Auditable")]);
213        assert_eq!(
214            clone.all(),
215            &[String::from("Auditable"), String::from("Trackable")]
216        );
217    }
218
219    #[test]
220    fn concern_registry_treats_empty_and_whitespace_names_as_distinct() {
221        let mut registry = ConcernRegistry::new();
222        registry.register("");
223        registry.register(" ");
224
225        assert!(registry.includes(""));
226        assert!(registry.includes(" "));
227        assert_eq!(registry.all(), &[String::new(), String::from(" ")]);
228    }
229
230    #[test]
231    fn concern_registry_preserves_first_occurrence_across_interleaved_duplicates() {
232        let mut registry = ConcernRegistry::new();
233        registry.register("Auditable");
234        registry.register("Trackable");
235        registry.register("Auditable");
236        registry.register("Publishable");
237        registry.register("Trackable");
238
239        assert_eq!(
240            registry.all(),
241            &[
242                String::from("Auditable"),
243                String::from("Trackable"),
244                String::from("Publishable"),
245            ]
246        );
247    }
248
249    #[test]
250    fn multiple_marker_types_can_implement_concern_trait() {
251        fn assert_concern<T: Concern>() {}
252
253        assert_concern::<Auditable>();
254        assert_concern::<Trackable>();
255        assert_concern::<Publishable>();
256    }
257}