use crate::Apps;
use crate::registry::{
ModelMetadata, ReverseRelationMetadata, ReverseRelationType, get_models_for_app,
get_registered_models, register_reverse_relation,
};
use std::borrow::Cow;
pub fn discover_models(app_label: &str) -> Vec<&'static ModelMetadata> {
get_models_for_app(app_label)
}
pub fn discover_all_models() -> &'static [ModelMetadata] {
get_registered_models()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelationMetadata {
pub from_model: &'static str,
pub to_model: &'static str,
pub field_name: &'static str,
pub related_name: Option<&'static str>,
pub relation_type: RelationType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationType {
OneToMany,
ManyToMany,
OneToOne,
}
impl RelationMetadata {
pub const fn new(
from_model: &'static str,
to_model: &'static str,
field_name: &'static str,
related_name: Option<&'static str>,
relation_type: RelationType,
) -> Self {
Self {
from_model,
to_model,
field_name,
related_name,
relation_type,
}
}
pub fn reverse_name(&self) -> Cow<'static, str> {
if let Some(name) = self.related_name {
Cow::Borrowed(name)
} else {
Cow::Owned(format!("{}_set", self.from_model.to_lowercase()))
}
}
}
pub fn build_reverse_relations() -> Result<(), crate::AppError> {
let models = get_registered_models();
let mut relations = Vec::new();
for model in models {
let model_relations = extract_model_relations(model);
relations.extend(model_relations);
}
for relation in &relations {
create_reverse_relation(relation)?;
}
Ok(())
}
fn extract_model_relations(model: &ModelMetadata) -> Vec<RelationMetadata> {
use crate::registry::get_relationships_for_model;
let qualified_name = model.qualified_name();
let relationships = get_relationships_for_model(&qualified_name);
relationships
.into_iter()
.map(|rel| {
let relation_type = match rel.relationship_type {
crate::registry::RelationshipType::ForeignKey => RelationType::OneToMany,
crate::registry::RelationshipType::ManyToMany => RelationType::ManyToMany,
crate::registry::RelationshipType::OneToOne => RelationType::OneToOne,
};
let from_model = rel.from_model_name();
let to_model = rel.to_model_name();
RelationMetadata::new(
from_model,
to_model,
rel.field_name,
rel.related_name,
relation_type,
)
})
.collect()
}
pub fn create_reverse_relation(relation: &RelationMetadata) -> Result<(), crate::AppError> {
let reverse_name = relation.reverse_name().into_owned();
let reverse_type = match relation.relation_type {
RelationType::OneToMany => ReverseRelationType::ReverseOneToMany,
RelationType::ManyToMany => ReverseRelationType::ReverseManyToMany,
RelationType::OneToOne => ReverseRelationType::ReverseOneToOne,
};
let reverse_relation = ReverseRelationMetadata::new(
relation.to_model, reverse_name, relation.from_model, reverse_type, relation.field_name, );
register_reverse_relation(reverse_relation)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrationMetadata {
pub app_label: String,
pub name: String,
pub number: u32,
pub path: std::path::PathBuf,
pub dependencies: Vec<String>,
}
impl MigrationMetadata {
pub fn new(
app_label: String,
name: String,
number: u32,
path: std::path::PathBuf,
dependencies: Vec<String>,
) -> Self {
Self {
app_label,
name,
number,
path,
dependencies,
}
}
pub fn qualified_name(&self) -> String {
format!("{}.{:04}_{}", self.app_label, self.number, self.name)
}
}
pub fn discover_migrations(apps: &Apps) -> Result<Vec<MigrationMetadata>, String> {
use std::fs;
use std::path::PathBuf;
let mut migrations = Vec::new();
for app in apps.get_app_configs() {
let Some(app_path_str) = app.path else {
continue;
};
let app_path = PathBuf::from(&app_path_str);
let migrations_dir = app_path.join("migrations");
if !migrations_dir.exists() || !migrations_dir.is_dir() {
continue;
}
let entries = fs::read_dir(&migrations_dir)
.map_err(|e| format!("Failed to read migrations directory: {}", e))?;
for entry in entries {
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|s| s.to_str()) != Some("rs") {
continue;
}
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if !file_name.starts_with(|c: char| c.is_ascii_digit()) {
continue;
}
match parse_migration_file(&path, &app.label) {
Ok(migration) => migrations.push(migration),
Err(_) => continue,
}
}
}
Ok(migrations)
}
fn parse_migration_file(
path: &std::path::Path,
app_label: &str,
) -> Result<MigrationMetadata, String> {
let filename = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| "Invalid filename".to_string())?;
let parts: Vec<&str> = filename.splitn(2, '_').collect();
if parts.len() != 2 {
return Err(format!("Invalid migration filename format: {}", filename));
}
let number = parts[0]
.parse::<u32>()
.map_err(|_| format!("Invalid migration number: {}", parts[0]))?;
let name = parts[1].to_string();
Ok(MigrationMetadata::new(
app_label.to_string(),
name,
number,
path.to_path_buf(),
vec![], ))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry::{MODELS, ModelMetadata, ReverseRelationType};
use linkme::distributed_slice;
use rstest::*;
use serial_test::serial;
#[distributed_slice(MODELS)]
static DISCOVERY_TEST_USER: ModelMetadata = ModelMetadata {
app_label: "discovery_test",
model_name: "User",
table_name: "discovery_test_users",
};
#[distributed_slice(MODELS)]
static DISCOVERY_TEST_POST: ModelMetadata = ModelMetadata {
app_label: "discovery_test",
model_name: "Post",
table_name: "discovery_test_posts",
};
#[test]
fn test_relation_metadata_new() {
let relation = RelationMetadata::new(
"Post",
"User",
"author",
Some("posts"),
RelationType::OneToMany,
);
assert_eq!(relation.from_model, "Post");
assert_eq!(relation.to_model, "User");
assert_eq!(relation.field_name, "author");
assert_eq!(relation.related_name, Some("posts"));
assert_eq!(relation.relation_type, RelationType::OneToMany);
}
#[test]
fn test_relation_metadata_reverse_name() {
let relation = RelationMetadata::new(
"Post",
"User",
"author",
Some("posts"),
RelationType::OneToMany,
);
assert_eq!(relation.reverse_name(), "posts");
let relation =
RelationMetadata::new("Post", "User", "author", None, RelationType::OneToMany);
assert_eq!(relation.reverse_name(), "post_set");
}
#[test]
fn test_relation_types() {
assert_eq!(RelationType::OneToMany, RelationType::OneToMany);
assert_ne!(RelationType::OneToMany, RelationType::ManyToMany);
assert_ne!(RelationType::OneToMany, RelationType::OneToOne);
}
#[test]
fn test_migration_metadata_new() {
use std::path::PathBuf;
let migration = MigrationMetadata::new(
"myapp".to_string(),
"initial".to_string(),
1,
PathBuf::from("/tmp/migrations/0001_initial.rs"),
vec![],
);
assert_eq!(migration.app_label, "myapp");
assert_eq!(migration.name, "initial");
assert_eq!(migration.number, 1);
assert_eq!(migration.dependencies.len(), 0);
}
#[test]
fn test_migration_metadata_qualified_name() {
use std::path::PathBuf;
let migration = MigrationMetadata::new(
"myapp".to_string(),
"initial".to_string(),
1,
PathBuf::from("/tmp/migrations/0001_initial.rs"),
vec![],
);
assert_eq!(migration.qualified_name(), "myapp.0001_initial");
}
#[test]
fn test_migration_metadata_with_dependencies() {
use std::path::PathBuf;
let migration = MigrationMetadata::new(
"myapp".to_string(),
"add_field".to_string(),
2,
PathBuf::from("/tmp/migrations/0002_add_field.rs"),
vec![
"myapp.0001_initial".to_string(),
"auth.0001_initial".to_string(),
],
);
assert_eq!(migration.dependencies.len(), 2);
assert_eq!(migration.dependencies[0], "myapp.0001_initial");
assert_eq!(migration.dependencies[1], "auth.0001_initial");
}
#[test]
fn test_parse_migration_file() {
use std::path::PathBuf;
let path = PathBuf::from("/tmp/migrations/0001_initial.rs");
let result = parse_migration_file(&path, "myapp");
let migration = result.unwrap();
assert_eq!(migration.app_label, "myapp");
assert_eq!(migration.name, "initial");
assert_eq!(migration.number, 1);
}
#[test]
fn test_parse_migration_file_with_underscores() {
use std::path::PathBuf;
let path = PathBuf::from("/tmp/migrations/0002_add_user_field.rs");
let result = parse_migration_file(&path, "myapp");
let migration = result.unwrap();
assert_eq!(migration.app_label, "myapp");
assert_eq!(migration.name, "add_user_field");
assert_eq!(migration.number, 2);
}
#[test]
fn test_parse_migration_file_invalid_format() {
use std::path::PathBuf;
let path = PathBuf::from("/tmp/migrations/0001initial.rs");
let result = parse_migration_file(&path, "myapp");
assert!(result.is_err());
let path = PathBuf::from("/tmp/migrations/abc_initial.rs");
let result = parse_migration_file(&path, "myapp");
assert!(result.is_err());
}
#[rstest]
#[case(RelationType::OneToMany, ReverseRelationType::ReverseOneToMany)]
#[case(RelationType::ManyToMany, ReverseRelationType::ReverseManyToMany)]
#[case(RelationType::OneToOne, ReverseRelationType::ReverseOneToOne)]
#[serial(apps_registry)]
fn test_create_reverse_relation_uses_static_fields_directly(
#[case] relation_type: RelationType,
#[case] expected_reverse_type: ReverseRelationType,
) {
crate::registry::reset_global_registry();
let relation = RelationMetadata::new(
"Article",
"Author",
"writer",
Some("articles"),
relation_type,
);
create_reverse_relation(&relation).expect("reverse relation registration should succeed");
let reverse_relations = crate::registry::get_reverse_relations_for_model("Author");
let found = reverse_relations
.iter()
.find(|r| r.accessor_name == "articles");
if let Some(rev) = found {
assert_eq!(rev.on_model, "Author");
assert_eq!(rev.related_model, "Article");
assert_eq!(rev.through_field, "writer");
assert_eq!(rev.relation_type, expected_reverse_type);
assert!(std::ptr::eq(rev.on_model, relation.to_model));
assert!(std::ptr::eq(rev.related_model, relation.from_model));
assert!(std::ptr::eq(rev.through_field, relation.field_name));
}
}
#[rstest]
#[serial(apps_registry)]
fn test_create_reverse_relation_default_accessor_name() {
crate::registry::reset_global_registry();
let relation =
RelationMetadata::new("Comment", "BlogPost", "post", None, RelationType::OneToMany);
create_reverse_relation(&relation).expect("reverse relation registration should succeed");
let reverse_relations = crate::registry::get_reverse_relations_for_model("BlogPost");
let found = reverse_relations
.iter()
.find(|r| r.accessor_name == "comment_set");
if let Some(rev) = found {
assert_eq!(rev.on_model, "BlogPost");
assert_eq!(rev.accessor_name, "comment_set");
assert_eq!(rev.related_model, "Comment");
assert_eq!(rev.relation_type, ReverseRelationType::ReverseOneToMany);
}
}
}