rustrails_record/
reflection.rs1use std::collections::HashMap;
2
3use serde_json::{Value, json};
4
5use crate::associations::{AssociationMeta, AssociationType, HasAssociations};
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct AssociationReflection {
10 pub kind: AssociationType,
12 pub name: String,
14 pub options: HashMap<String, Value>,
16}
17
18pub trait Reflection: HasAssociations {
20 #[must_use]
22 fn reflect_on_association(name: &str) -> Option<AssociationReflection> {
23 Self::associations()
24 .get(name)
25 .map(AssociationReflection::from)
26 }
27
28 #[must_use]
30 fn reflect_on_all_associations() -> Vec<AssociationReflection> {
31 Self::associations()
32 .all()
33 .iter()
34 .map(AssociationReflection::from)
35 .collect()
36 }
37}
38
39impl From<&AssociationMeta> for AssociationReflection {
40 fn from(meta: &AssociationMeta) -> Self {
41 let mut options = HashMap::from([
42 (
43 "target_table".to_owned(),
44 Value::String(meta.target_table.clone()),
45 ),
46 (
47 "foreign_key".to_owned(),
48 Value::String(meta.foreign_key.clone()),
49 ),
50 (
51 "primary_key".to_owned(),
52 Value::String(meta.primary_key.clone()),
53 ),
54 ("polymorphic".to_owned(), Value::Bool(meta.polymorphic)),
55 ]);
56
57 if let Some(through) = &meta.through {
58 options.insert("through".to_owned(), Value::String(through.clone()));
59 }
60 if let Some(dependent) = meta.dependent {
61 options.insert("dependent".to_owned(), json!(format!("{dependent:?}")));
62 }
63
64 Self {
65 kind: meta.association_type,
66 name: meta.name.clone(),
67 options,
68 }
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use std::sync::LazyLock;
75
76 use sea_orm::{ActiveModelTrait, EntityTrait};
77 use serde_json::json;
78
79 use super::{AssociationReflection, Reflection};
80 use crate::{
81 Record, RecordState,
82 associations::{
83 AssociationRegistry, AssociationType, BelongsToBuilder, DependentAction,
84 HasAndBelongsToManyBuilder, HasAssociations, HasManyBuilder, HasOneBuilder,
85 },
86 base::test_support::test_user,
87 };
88
89 #[derive(Debug, Default)]
90 struct ReflectionRecord {
91 state: RecordState,
92 }
93
94 impl Record for ReflectionRecord {
95 type Entity = test_user::Entity;
96
97 fn table_name() -> &'static str {
98 "test_users"
99 }
100
101 fn id(&self) -> Option<i64> {
102 None
103 }
104
105 fn record_state(&self) -> RecordState {
106 self.state
107 }
108
109 fn set_record_state(&mut self, state: RecordState) {
110 self.state = state;
111 }
112
113 fn from_sea_model(_model: <Self::Entity as EntityTrait>::Model) -> Self {
114 Self {
115 state: RecordState::Persisted,
116 }
117 }
118
119 fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
120 where
121 <Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
122 {
123 <test_user::ActiveModel as Default>::default()
124 }
125 }
126
127 static TEST_ASSOCIATIONS: LazyLock<AssociationRegistry> = LazyLock::new(|| {
128 let mut registry = AssociationRegistry::new();
129 registry.add(
130 HasManyBuilder::new("comments")
131 .dependent(DependentAction::Destroy)
132 .build(),
133 );
134 registry.add(HasOneBuilder::new("profile").build());
135 registry.add(BelongsToBuilder::new("account").build());
136 registry.add(
137 HasAndBelongsToManyBuilder::new("roles")
138 .through("accounts_roles")
139 .build(),
140 );
141 registry
142 });
143
144 impl HasAssociations for ReflectionRecord {
145 fn associations() -> &'static AssociationRegistry {
146 &TEST_ASSOCIATIONS
147 }
148 }
149
150 impl Reflection for ReflectionRecord {}
151
152 #[test]
153 fn reflect_on_association_returns_matching_metadata() {
154 let reflection = ReflectionRecord::reflect_on_association("comments")
155 .expect("comments reflection should exist");
156
157 assert_eq!(reflection.kind, AssociationType::HasMany);
158 assert_eq!(reflection.name, "comments");
159 assert_eq!(reflection.options.get("dependent"), Some(&json!("Destroy")));
160 }
161
162 #[test]
163 fn reflect_on_association_returns_none_for_unknown_name() {
164 assert!(ReflectionRecord::reflect_on_association("missing").is_none());
165 }
166
167 #[test]
168 fn reflect_on_all_associations_preserves_declaration_order() {
169 let names = ReflectionRecord::reflect_on_all_associations()
170 .into_iter()
171 .map(|reflection| reflection.name)
172 .collect::<Vec<_>>();
173
174 assert_eq!(names, vec!["comments", "profile", "account", "roles"]);
175 }
176
177 #[test]
178 fn reflection_includes_through_option_when_present() {
179 let reflection = ReflectionRecord::reflect_on_association("roles")
180 .expect("roles reflection should exist");
181 assert_eq!(
182 reflection.options.get("through"),
183 Some(&json!("accounts_roles"))
184 );
185 }
186
187 #[test]
188 fn reflection_includes_core_key_options() {
189 let reflection = ReflectionRecord::reflect_on_association("account")
190 .expect("account reflection should exist");
191 assert!(reflection.options.contains_key("target_table"));
192 assert!(reflection.options.contains_key("foreign_key"));
193 assert!(reflection.options.contains_key("primary_key"));
194 assert!(reflection.options.contains_key("polymorphic"));
195 }
196
197 #[test]
198 fn reflection_omits_optional_keys_when_not_configured() {
199 let reflection = ReflectionRecord::reflect_on_association("profile")
200 .expect("profile reflection should exist");
201
202 assert!(!reflection.options.contains_key("through"));
203 assert!(!reflection.options.contains_key("dependent"));
204 }
205
206 #[test]
207 fn reflection_with_dependent_does_not_include_null_through_option() {
208 let reflection = ReflectionRecord::reflect_on_association("comments")
209 .expect("comments reflection should exist");
210
211 assert!(!reflection.options.contains_key("through"));
212 assert_eq!(reflection.options.get("dependent"), Some(&json!("Destroy")));
213 }
214
215 #[test]
216 fn reflection_with_through_does_not_include_null_dependent_option() {
217 let reflection = ReflectionRecord::reflect_on_association("roles")
218 .expect("roles reflection should exist");
219
220 assert_eq!(
221 reflection.options.get("through"),
222 Some(&json!("accounts_roles"))
223 );
224 assert!(!reflection.options.contains_key("dependent"));
225 }
226
227 #[test]
228 fn reflection_options_match_underlying_metadata_values() {
229 let meta = ReflectionRecord::associations()
230 .get("account")
231 .expect("account meta should exist");
232 let reflection = AssociationReflection::from(meta);
233
234 assert_eq!(
235 reflection.options.get("target_table"),
236 Some(&json!(meta.target_table.as_str()))
237 );
238 assert_eq!(
239 reflection.options.get("foreign_key"),
240 Some(&json!(meta.foreign_key.as_str()))
241 );
242 assert_eq!(
243 reflection.options.get("primary_key"),
244 Some(&json!(meta.primary_key.as_str()))
245 );
246 assert_eq!(
247 reflection.options.get("polymorphic"),
248 Some(&json!(meta.polymorphic))
249 );
250 }
251
252 #[test]
253 fn reflect_on_all_associations_round_trips_individual_lookups() {
254 for reflection in ReflectionRecord::reflect_on_all_associations() {
255 assert_eq!(
256 ReflectionRecord::reflect_on_association(&reflection.name),
257 Some(reflection)
258 );
259 }
260 }
261
262 #[test]
263 fn association_reflection_can_be_built_from_meta() {
264 let meta = ReflectionRecord::associations()
265 .get("profile")
266 .expect("meta should exist");
267 let reflection = AssociationReflection::from(meta);
268
269 assert_eq!(reflection.kind, AssociationType::HasOne);
270 assert_eq!(reflection.name, "profile");
271 }
272}