use linkme::distributed_slice;
use std::collections::HashMap;
use std::sync::{OnceLock, PoisonError, RwLock};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelMetadata {
pub app_label: &'static str,
pub model_name: &'static str,
pub table_name: &'static str,
}
impl ModelMetadata {
pub const fn new(
app_label: &'static str,
model_name: &'static str,
table_name: &'static str,
) -> Self {
Self {
app_label,
model_name,
table_name,
}
}
pub fn qualified_name(&self) -> String {
format!("{}.{}", self.app_label, self.model_name)
}
}
#[distributed_slice]
pub static MODELS: [ModelMetadata];
static MODEL_CACHE: OnceLock<HashMap<&'static str, Vec<&'static ModelMetadata>>> = OnceLock::new();
fn model_cache() -> &'static HashMap<&'static str, Vec<&'static ModelMetadata>> {
MODEL_CACHE.get_or_init(|| {
let mut cache: HashMap<&'static str, Vec<&'static ModelMetadata>> = HashMap::new();
for model in MODELS.iter() {
cache.entry(model.app_label).or_default().push(model);
}
cache
})
}
pub fn get_registered_models() -> &'static [ModelMetadata] {
&MODELS
}
pub fn get_models_for_app(app_label: &str) -> Vec<&'static ModelMetadata> {
model_cache().get(app_label).cloned().unwrap_or_default()
}
pub fn find_model(qualified_name: &str) -> Option<&'static ModelMetadata> {
let parts: Vec<&str> = qualified_name.split('.').collect();
if parts.len() != 2 {
return None;
}
let (app_label, model_name) = (parts[0], parts[1]);
MODELS
.iter()
.find(|m| m.app_label == app_label && m.model_name == model_name)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelationshipMetadata {
pub from_model: &'static str,
pub to_model: &'static str,
pub relationship_type: RelationshipType,
pub field_name: &'static str,
pub related_name: Option<&'static str>,
pub db_column: Option<&'static str>,
pub through_table: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationshipType {
ForeignKey,
ManyToMany,
OneToOne,
}
impl RelationshipMetadata {
pub const fn new(
from_model: &'static str,
to_model: &'static str,
relationship_type: RelationshipType,
field_name: &'static str,
related_name: Option<&'static str>,
db_column: Option<&'static str>,
through_table: Option<&'static str>,
) -> Self {
Self {
from_model,
to_model,
relationship_type,
field_name,
related_name,
db_column,
through_table,
}
}
pub fn from_model_name(&self) -> &str {
self.from_model
.split('.')
.next_back()
.unwrap_or(self.from_model)
}
pub fn to_model_name(&self) -> &str {
self.to_model
.split('.')
.next_back()
.unwrap_or(self.to_model)
}
}
#[distributed_slice]
pub static RELATIONSHIPS: [RelationshipMetadata];
static RELATIONSHIP_CACHE: OnceLock<HashMap<&'static str, Vec<&'static RelationshipMetadata>>> =
OnceLock::new();
fn relationship_cache() -> &'static HashMap<&'static str, Vec<&'static RelationshipMetadata>> {
RELATIONSHIP_CACHE.get_or_init(|| {
let mut cache: HashMap<&'static str, Vec<&'static RelationshipMetadata>> = HashMap::new();
for rel in RELATIONSHIPS.iter() {
cache.entry(rel.from_model).or_default().push(rel);
}
cache
})
}
pub fn get_registered_relationships() -> &'static [RelationshipMetadata] {
&RELATIONSHIPS
}
pub fn get_relationships_for_model(model: &str) -> Vec<&'static RelationshipMetadata> {
relationship_cache().get(model).cloned().unwrap_or_default()
}
pub fn get_relationships_to_model(target_model: &str) -> Vec<&'static RelationshipMetadata> {
RELATIONSHIPS
.iter()
.filter(|r| r.to_model == target_model)
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReverseRelationMetadata {
pub on_model: &'static str,
pub accessor_name: String,
pub related_model: &'static str,
pub relation_type: ReverseRelationType,
pub through_field: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReverseRelationType {
ReverseOneToMany,
ReverseManyToMany,
ReverseOneToOne,
}
impl ReverseRelationMetadata {
pub fn new(
on_model: &'static str,
accessor_name: String,
related_model: &'static str,
relation_type: ReverseRelationType,
through_field: &'static str,
) -> Self {
Self {
on_model,
accessor_name,
related_model,
relation_type,
through_field,
}
}
}
static REVERSE_RELATIONS_BUILDER: RwLock<Vec<ReverseRelationMetadata>> = RwLock::new(Vec::new());
static REVERSE_RELATIONS: OnceLock<HashMap<String, Vec<ReverseRelationMetadata>>> = OnceLock::new();
pub fn register_reverse_relation(relation: ReverseRelationMetadata) -> Result<(), crate::AppError> {
if REVERSE_RELATIONS.get().is_some() {
return Err(crate::AppError::RegistryState(
"Cannot register reverse relations after finalization".to_string(),
));
}
let mut builder = REVERSE_RELATIONS_BUILDER
.write()
.unwrap_or_else(PoisonError::into_inner);
builder.push(relation);
Ok(())
}
pub fn finalize_reverse_relations() {
if REVERSE_RELATIONS.get().is_some() {
return;
}
let builder = REVERSE_RELATIONS_BUILDER
.read()
.unwrap_or_else(PoisonError::into_inner);
let mut indexed = HashMap::new();
for relation in builder.iter() {
indexed
.entry(relation.on_model.to_string())
.or_insert_with(Vec::new)
.push(relation.clone());
}
let _ = REVERSE_RELATIONS.set(indexed);
}
pub fn get_reverse_relations_for_model(model_name: &str) -> Vec<ReverseRelationMetadata> {
REVERSE_RELATIONS
.get()
.and_then(|m| m.get(model_name))
.cloned()
.unwrap_or_default()
}
#[cfg(any(test, feature = "testing"))]
pub fn reset_global_registry() {
use std::sync::PoisonError;
let mut builder = REVERSE_RELATIONS_BUILDER
.write()
.unwrap_or_else(PoisonError::into_inner);
builder.clear();
drop(builder);
unsafe {
let model_cache_ptr = std::ptr::addr_of!(MODEL_CACHE)
as *mut OnceLock<HashMap<&'static str, Vec<&'static ModelMetadata>>>;
std::ptr::write(model_cache_ptr, OnceLock::new());
let rel_cache_ptr = std::ptr::addr_of!(RELATIONSHIP_CACHE)
as *mut OnceLock<HashMap<&'static str, Vec<&'static RelationshipMetadata>>>;
std::ptr::write(rel_cache_ptr, OnceLock::new());
let rev_rel_ptr = std::ptr::addr_of!(REVERSE_RELATIONS)
as *mut OnceLock<HashMap<String, Vec<ReverseRelationMetadata>>>;
std::ptr::write(rev_rel_ptr, OnceLock::new());
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::*;
#[distributed_slice(MODELS)]
static TEST_USER_MODEL: ModelMetadata = ModelMetadata {
app_label: "auth",
model_name: "User",
table_name: "auth_users",
};
#[distributed_slice(MODELS)]
static TEST_POST_MODEL: ModelMetadata = ModelMetadata {
app_label: "blog",
model_name: "Post",
table_name: "blog_posts",
};
#[distributed_slice(MODELS)]
static TEST_COMMENT_MODEL: ModelMetadata = ModelMetadata {
app_label: "blog",
model_name: "Comment",
table_name: "blog_comments",
};
#[test]
fn test_model_metadata_new() {
let metadata = ModelMetadata::new("myapp", "MyModel", "my_table");
assert_eq!(metadata.app_label, "myapp");
assert_eq!(metadata.model_name, "MyModel");
assert_eq!(metadata.table_name, "my_table");
}
#[test]
fn test_qualified_name() {
let metadata = ModelMetadata::new("auth", "User", "users");
assert_eq!(metadata.qualified_name(), "auth.User");
}
#[test]
fn test_find_model_invalid_format() {
let model = find_model("InvalidFormat");
assert!(model.is_none());
let model = find_model("too.many.parts");
assert!(model.is_none());
}
#[test]
fn test_model_metadata_equality() {
let meta1 = ModelMetadata::new("app", "Model", "table");
let meta2 = ModelMetadata::new("app", "Model", "table");
let meta3 = ModelMetadata::new("app", "Other", "table");
assert_eq!(meta1, meta2);
assert_ne!(meta1, meta3);
}
#[test]
fn test_reverse_relation_metadata_new() {
let relation = ReverseRelationMetadata::new(
"User",
"posts".to_string(),
"Post",
ReverseRelationType::ReverseOneToMany,
"author",
);
assert_eq!(relation.on_model, "User");
assert_eq!(relation.accessor_name, "posts");
assert_eq!(relation.related_model, "Post");
assert_eq!(
relation.relation_type,
ReverseRelationType::ReverseOneToMany
);
assert_eq!(relation.through_field, "author");
}
#[rstest]
fn test_get_reverse_relations_for_nonexistent_model() {
let relations = get_reverse_relations_for_model("NonExistent");
assert!(relations.is_empty());
}
#[test]
fn test_reverse_relation_types() {
assert_eq!(
ReverseRelationType::ReverseOneToMany,
ReverseRelationType::ReverseOneToMany
);
assert_ne!(
ReverseRelationType::ReverseOneToMany,
ReverseRelationType::ReverseManyToMany
);
assert_ne!(
ReverseRelationType::ReverseOneToMany,
ReverseRelationType::ReverseOneToOne
);
}
#[distributed_slice(RELATIONSHIPS)]
static TEST_POST_AUTHOR: RelationshipMetadata = RelationshipMetadata {
from_model: "blog.Post",
to_model: "auth.User",
relationship_type: RelationshipType::ForeignKey,
field_name: "author",
related_name: Some("posts"),
db_column: Some("author_id"),
through_table: None,
};
#[distributed_slice(RELATIONSHIPS)]
static TEST_POST_TAGS: RelationshipMetadata = RelationshipMetadata {
from_model: "blog.Post",
to_model: "blog.Tag",
relationship_type: RelationshipType::ManyToMany,
field_name: "tags",
related_name: Some("posts"),
db_column: None,
through_table: Some("blog_post_tags"),
};
#[test]
fn test_relationship_metadata_new() {
let relationship = RelationshipMetadata::new(
"blog.Post",
"auth.User",
RelationshipType::ForeignKey,
"author",
Some("posts"),
Some("author_id"),
None,
);
assert_eq!(relationship.from_model, "blog.Post");
assert_eq!(relationship.to_model, "auth.User");
assert_eq!(relationship.relationship_type, RelationshipType::ForeignKey);
assert_eq!(relationship.field_name, "author");
assert_eq!(relationship.related_name, Some("posts"));
assert_eq!(relationship.db_column, Some("author_id"));
assert_eq!(relationship.through_table, None);
}
#[test]
fn test_relationship_metadata_model_names() {
let relationship = RelationshipMetadata::new(
"blog.Post",
"auth.User",
RelationshipType::ForeignKey,
"author",
None,
None,
None,
);
assert_eq!(relationship.from_model_name(), "Post");
assert_eq!(relationship.to_model_name(), "User");
}
#[test]
fn test_relationship_types() {
assert_eq!(RelationshipType::ForeignKey, RelationshipType::ForeignKey);
assert_ne!(RelationshipType::ForeignKey, RelationshipType::ManyToMany);
assert_ne!(RelationshipType::ForeignKey, RelationshipType::OneToOne);
}
#[test]
fn test_relationship_metadata_equality() {
let rel1 = RelationshipMetadata::new(
"app.Model",
"app.Other",
RelationshipType::ForeignKey,
"field",
None,
None,
None,
);
let rel2 = RelationshipMetadata::new(
"app.Model",
"app.Other",
RelationshipType::ForeignKey,
"field",
None,
None,
None,
);
let rel3 = RelationshipMetadata::new(
"app.Model",
"app.Other",
RelationshipType::ManyToMany,
"field",
None,
None,
None,
);
assert_eq!(rel1, rel2);
assert_ne!(rel1, rel3);
}
#[rstest]
fn test_rwlock_poison_recovery_write() {
let lock = RwLock::new(vec![1, 2, 3]);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = lock.write().unwrap();
panic!("intentional panic to poison the lock");
}));
let mut guard = lock.write().unwrap_or_else(PoisonError::into_inner);
guard.push(4);
assert_eq!(*guard, vec![1, 2, 3, 4]);
}
#[rstest]
fn test_rwlock_poison_recovery_read() {
let lock = RwLock::new(vec![10, 20, 30]);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = lock.write().unwrap();
panic!("intentional panic to poison the lock");
}));
let guard = lock.read().unwrap_or_else(PoisonError::into_inner);
assert_eq!(*guard, vec![10, 20, 30]);
}
}