use std::collections::HashMap;
use super::types::AdminEntry;
pub const RELATION_FILTER_DROPDOWN_CAP: usize = 500;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRelation {
pub source_model: String,
pub source_field: String,
pub target_model: String,
pub target_table: String,
pub target_admin_name: String,
pub target_display_field: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InverseRelation {
pub source_model: String,
pub source_table: String,
pub source_admin_name: String,
pub source_display_name: String,
pub source_field: String,
pub target_model: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryError {
UnknownTarget {
model: String,
field: String,
target: String,
},
UnknownDisplayField {
model: String,
field: String,
target: String,
display: String,
},
}
impl std::fmt::Display for RegistryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownTarget {
model,
field,
target,
} => write!(
f,
"`{model}.{field}` declares `belongs_to = \"{target}\"`, \
but no admin entry named `{target}` is registered"
),
Self::UnknownDisplayField {
model,
field,
target,
display,
} => write!(
f,
"`{model}.{field}` declares `display = \"{display}\"` against `{target}`, \
but `{target}` has no field named `{display}`"
),
}
}
}
impl std::error::Error for RegistryError {}
#[derive(Debug, Clone, Default)]
pub struct RelationRegistry {
belongs_to: HashMap<(String, String), ResolvedRelation>,
has_many: HashMap<String, Vec<InverseRelation>>,
belongs_to_of: HashMap<String, Vec<ResolvedRelation>>,
}
impl RelationRegistry {
pub fn empty() -> Self {
Self::default()
}
pub fn from_admin_entries(entries: &[AdminEntry]) -> Self {
let mut belongs_to: HashMap<(String, String), ResolvedRelation> = HashMap::new();
let mut has_many: HashMap<String, Vec<InverseRelation>> = HashMap::new();
let mut belongs_to_of: HashMap<String, Vec<ResolvedRelation>> = HashMap::new();
let by_singular: HashMap<&str, &AdminEntry> =
entries.iter().map(|e| (e.singular_name, e)).collect();
for source in entries {
for field in source.fields {
let Some(rel) = &field.relation else {
continue;
};
let Some(target) = by_singular.get(rel.target_model) else {
continue;
};
let display_field = match rel.display_field {
None => None,
Some(col) => {
if target.fields.iter().any(|f| f.name == col) {
Some(col.to_string())
} else {
None
}
}
};
let resolved = ResolvedRelation {
source_model: source.singular_name.to_string(),
source_field: field.name.to_string(),
target_model: target.singular_name.to_string(),
target_table: target.table.to_string(),
target_admin_name: target.admin_name.to_string(),
target_display_field: display_field,
};
belongs_to.insert(
(source.singular_name.to_string(), field.name.to_string()),
resolved.clone(),
);
belongs_to_of
.entry(source.singular_name.to_string())
.or_default()
.push(resolved.clone());
has_many
.entry(target.singular_name.to_string())
.or_default()
.push(InverseRelation {
source_model: source.singular_name.to_string(),
source_table: source.table.to_string(),
source_admin_name: source.admin_name.to_string(),
source_display_name: source.display_name.to_string(),
source_field: field.name.to_string(),
target_model: target.singular_name.to_string(),
});
}
}
for list in has_many.values_mut() {
list.sort_by(|a, b| a.source_model.cmp(&b.source_model));
}
for list in belongs_to_of.values_mut() {
list.sort_by(|a, b| a.source_field.cmp(&b.source_field));
}
Self {
belongs_to,
has_many,
belongs_to_of,
}
}
pub fn belongs_to(&self, model: &str, field: &str) -> Option<&ResolvedRelation> {
self.belongs_to.get(&(model.to_string(), field.to_string()))
}
pub fn belongs_to_of(&self, model: &str) -> &[ResolvedRelation] {
self.belongs_to_of
.get(model)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn has_many(&self, model: &str) -> &[InverseRelation] {
self.has_many
.get(model)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn is_empty(&self) -> bool {
self.belongs_to.is_empty()
}
pub fn validate(&self, entries: &[AdminEntry]) -> Vec<RegistryError> {
let mut errors: Vec<RegistryError> = Vec::new();
let by_singular: HashMap<&str, &AdminEntry> =
entries.iter().map(|e| (e.singular_name, e)).collect();
for source in entries {
for field in source.fields {
let Some(rel) = &field.relation else {
continue;
};
let Some(target) = by_singular.get(rel.target_model) else {
errors.push(RegistryError::UnknownTarget {
model: source.singular_name.to_string(),
field: field.name.to_string(),
target: rel.target_model.to_string(),
});
continue;
};
if let Some(display) = rel.display_field {
if !target.fields.iter().any(|f| f.name == display) {
errors.push(RegistryError::UnknownDisplayField {
model: source.singular_name.to_string(),
field: field.name.to_string(),
target: rel.target_model.to_string(),
display: display.to_string(),
});
}
}
}
}
errors
}
pub fn iter_belongs_to(&self) -> impl Iterator<Item = &ResolvedRelation> {
let mut entries: Vec<&ResolvedRelation> = self.belongs_to.values().collect();
entries.sort_by(|a, b| {
a.source_model
.cmp(&b.source_model)
.then_with(|| a.source_field.cmp(&b.source_field))
});
entries.into_iter()
}
}