rustrails_support/
concern.rs1pub trait Concern: Send + Sync {}
3
4#[derive(Debug, Default, Clone, PartialEq, Eq)]
6pub struct ConcernRegistry {
7 concerns: Vec<String>,
8}
9
10impl ConcernRegistry {
11 #[must_use]
13 pub fn new() -> Self {
14 Self {
15 concerns: Vec::new(),
16 }
17 }
18
19 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 #[must_use]
29 pub fn includes(&self, name: &str) -> bool {
30 self.concerns.iter().any(|registered| registered == name)
31 }
32
33 #[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}