use std::collections::HashMap;
use serde_json::{Value, json};
use crate::associations::{AssociationMeta, AssociationType, HasAssociations};
#[derive(Debug, Clone, PartialEq)]
pub struct AssociationReflection {
pub kind: AssociationType,
pub name: String,
pub options: HashMap<String, Value>,
}
pub trait Reflection: HasAssociations {
#[must_use]
fn reflect_on_association(name: &str) -> Option<AssociationReflection> {
Self::associations()
.get(name)
.map(AssociationReflection::from)
}
#[must_use]
fn reflect_on_all_associations() -> Vec<AssociationReflection> {
Self::associations()
.all()
.iter()
.map(AssociationReflection::from)
.collect()
}
}
impl From<&AssociationMeta> for AssociationReflection {
fn from(meta: &AssociationMeta) -> Self {
let mut options = HashMap::from([
(
"target_table".to_owned(),
Value::String(meta.target_table.clone()),
),
(
"foreign_key".to_owned(),
Value::String(meta.foreign_key.clone()),
),
(
"primary_key".to_owned(),
Value::String(meta.primary_key.clone()),
),
("polymorphic".to_owned(), Value::Bool(meta.polymorphic)),
]);
if let Some(through) = &meta.through {
options.insert("through".to_owned(), Value::String(through.clone()));
}
if let Some(dependent) = meta.dependent {
options.insert("dependent".to_owned(), json!(format!("{dependent:?}")));
}
Self {
kind: meta.association_type,
name: meta.name.clone(),
options,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use sea_orm::{ActiveModelTrait, EntityTrait};
use serde_json::json;
use super::{AssociationReflection, Reflection};
use crate::{
Record, RecordState,
associations::{
AssociationRegistry, AssociationType, BelongsToBuilder, DependentAction,
HasAndBelongsToManyBuilder, HasAssociations, HasManyBuilder, HasOneBuilder,
},
base::test_support::test_user,
};
#[derive(Debug, Default)]
struct ReflectionRecord {
state: RecordState,
}
impl Record for ReflectionRecord {
type Entity = test_user::Entity;
fn table_name() -> &'static str {
"test_users"
}
fn id(&self) -> Option<i64> {
None
}
fn record_state(&self) -> RecordState {
self.state
}
fn set_record_state(&mut self, state: RecordState) {
self.state = state;
}
fn from_sea_model(_model: <Self::Entity as EntityTrait>::Model) -> Self {
Self {
state: RecordState::Persisted,
}
}
fn to_active_model(&self) -> <Self::Entity as EntityTrait>::ActiveModel
where
<Self::Entity as EntityTrait>::ActiveModel: ActiveModelTrait,
{
<test_user::ActiveModel as Default>::default()
}
}
static TEST_ASSOCIATIONS: LazyLock<AssociationRegistry> = LazyLock::new(|| {
let mut registry = AssociationRegistry::new();
registry.add(
HasManyBuilder::new("comments")
.dependent(DependentAction::Destroy)
.build(),
);
registry.add(HasOneBuilder::new("profile").build());
registry.add(BelongsToBuilder::new("account").build());
registry.add(
HasAndBelongsToManyBuilder::new("roles")
.through("accounts_roles")
.build(),
);
registry
});
impl HasAssociations for ReflectionRecord {
fn associations() -> &'static AssociationRegistry {
&TEST_ASSOCIATIONS
}
}
impl Reflection for ReflectionRecord {}
#[test]
fn reflect_on_association_returns_matching_metadata() {
let reflection = ReflectionRecord::reflect_on_association("comments")
.expect("comments reflection should exist");
assert_eq!(reflection.kind, AssociationType::HasMany);
assert_eq!(reflection.name, "comments");
assert_eq!(reflection.options.get("dependent"), Some(&json!("Destroy")));
}
#[test]
fn reflect_on_association_returns_none_for_unknown_name() {
assert!(ReflectionRecord::reflect_on_association("missing").is_none());
}
#[test]
fn reflect_on_all_associations_preserves_declaration_order() {
let names = ReflectionRecord::reflect_on_all_associations()
.into_iter()
.map(|reflection| reflection.name)
.collect::<Vec<_>>();
assert_eq!(names, vec!["comments", "profile", "account", "roles"]);
}
#[test]
fn reflection_includes_through_option_when_present() {
let reflection = ReflectionRecord::reflect_on_association("roles")
.expect("roles reflection should exist");
assert_eq!(
reflection.options.get("through"),
Some(&json!("accounts_roles"))
);
}
#[test]
fn reflection_includes_core_key_options() {
let reflection = ReflectionRecord::reflect_on_association("account")
.expect("account reflection should exist");
assert!(reflection.options.contains_key("target_table"));
assert!(reflection.options.contains_key("foreign_key"));
assert!(reflection.options.contains_key("primary_key"));
assert!(reflection.options.contains_key("polymorphic"));
}
#[test]
fn reflection_omits_optional_keys_when_not_configured() {
let reflection = ReflectionRecord::reflect_on_association("profile")
.expect("profile reflection should exist");
assert!(!reflection.options.contains_key("through"));
assert!(!reflection.options.contains_key("dependent"));
}
#[test]
fn reflection_with_dependent_does_not_include_null_through_option() {
let reflection = ReflectionRecord::reflect_on_association("comments")
.expect("comments reflection should exist");
assert!(!reflection.options.contains_key("through"));
assert_eq!(reflection.options.get("dependent"), Some(&json!("Destroy")));
}
#[test]
fn reflection_with_through_does_not_include_null_dependent_option() {
let reflection = ReflectionRecord::reflect_on_association("roles")
.expect("roles reflection should exist");
assert_eq!(
reflection.options.get("through"),
Some(&json!("accounts_roles"))
);
assert!(!reflection.options.contains_key("dependent"));
}
#[test]
fn reflection_options_match_underlying_metadata_values() {
let meta = ReflectionRecord::associations()
.get("account")
.expect("account meta should exist");
let reflection = AssociationReflection::from(meta);
assert_eq!(
reflection.options.get("target_table"),
Some(&json!(meta.target_table.as_str()))
);
assert_eq!(
reflection.options.get("foreign_key"),
Some(&json!(meta.foreign_key.as_str()))
);
assert_eq!(
reflection.options.get("primary_key"),
Some(&json!(meta.primary_key.as_str()))
);
assert_eq!(
reflection.options.get("polymorphic"),
Some(&json!(meta.polymorphic))
);
}
#[test]
fn reflect_on_all_associations_round_trips_individual_lookups() {
for reflection in ReflectionRecord::reflect_on_all_associations() {
assert_eq!(
ReflectionRecord::reflect_on_association(&reflection.name),
Some(reflection)
);
}
}
#[test]
fn association_reflection_can_be_built_from_meta() {
let meta = ReflectionRecord::associations()
.get("profile")
.expect("meta should exist");
let reflection = AssociationReflection::from(meta);
assert_eq!(reflection.kind, AssociationType::HasOne);
assert_eq!(reflection.name, "profile");
}
}