1use std::collections::HashMap;
22
23use super::types::AdminEntry;
24
25pub const RELATION_FILTER_DROPDOWN_CAP: usize = 500;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ResolvedRelation {
35 pub source_model: String,
37 pub source_field: String,
39 pub target_model: String,
41 pub target_table: String,
43 pub target_admin_name: String,
45 pub target_display_field: Option<String>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct InverseRelation {
56 pub source_model: String,
58 pub source_table: String,
60 pub source_admin_name: String,
62 pub source_display_name: String,
64 pub source_field: String,
66 pub target_model: String,
68}
69
70#[non_exhaustive]
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum RegistryError {
74 UnknownTarget {
77 model: String,
78 field: String,
79 target: String,
80 },
81 UnknownDisplayField {
83 model: String,
84 field: String,
85 target: String,
86 display: String,
87 },
88}
89
90impl std::fmt::Display for RegistryError {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Self::UnknownTarget {
94 model,
95 field,
96 target,
97 } => write!(
98 f,
99 "`{model}.{field}` declares `belongs_to = \"{target}\"`, \
100 but no admin entry named `{target}` is registered"
101 ),
102 Self::UnknownDisplayField {
103 model,
104 field,
105 target,
106 display,
107 } => write!(
108 f,
109 "`{model}.{field}` declares `display = \"{display}\"` against `{target}`, \
110 but `{target}` has no field named `{display}`"
111 ),
112 }
113 }
114}
115
116impl std::error::Error for RegistryError {}
117
118#[derive(Debug, Clone, Default)]
120pub struct RelationRegistry {
121 belongs_to: HashMap<(String, String), ResolvedRelation>,
122 has_many: HashMap<String, Vec<InverseRelation>>,
123 belongs_to_of: HashMap<String, Vec<ResolvedRelation>>,
125}
126
127impl RelationRegistry {
128 pub fn empty() -> Self {
130 Self::default()
131 }
132
133 pub fn from_admin_entries(entries: &[AdminEntry]) -> Self {
137 let mut belongs_to: HashMap<(String, String), ResolvedRelation> = HashMap::new();
138 let mut has_many: HashMap<String, Vec<InverseRelation>> = HashMap::new();
139 let mut belongs_to_of: HashMap<String, Vec<ResolvedRelation>> = HashMap::new();
140
141 let by_singular: HashMap<&str, &AdminEntry> =
143 entries.iter().map(|e| (e.singular_name, e)).collect();
144
145 for source in entries {
146 for field in source.fields {
147 let Some(rel) = &field.relation else {
148 continue;
149 };
150 let Some(target) = by_singular.get(rel.target_model) else {
151 continue;
152 };
153 let display_field = match rel.display_field {
156 None => None,
157 Some(col) => {
158 if target.fields.iter().any(|f| f.name == col) {
159 Some(col.to_string())
160 } else {
161 None
162 }
163 }
164 };
165
166 let resolved = ResolvedRelation {
167 source_model: source.singular_name.to_string(),
168 source_field: field.name.to_string(),
169 target_model: target.singular_name.to_string(),
170 target_table: target.table.to_string(),
171 target_admin_name: target.admin_name.to_string(),
172 target_display_field: display_field,
173 };
174
175 belongs_to.insert(
176 (source.singular_name.to_string(), field.name.to_string()),
177 resolved.clone(),
178 );
179
180 belongs_to_of
181 .entry(source.singular_name.to_string())
182 .or_default()
183 .push(resolved.clone());
184
185 has_many
186 .entry(target.singular_name.to_string())
187 .or_default()
188 .push(InverseRelation {
189 source_model: source.singular_name.to_string(),
190 source_table: source.table.to_string(),
191 source_admin_name: source.admin_name.to_string(),
192 source_display_name: source.display_name.to_string(),
193 source_field: field.name.to_string(),
194 target_model: target.singular_name.to_string(),
195 });
196 }
197 }
198
199 for list in has_many.values_mut() {
201 list.sort_by(|a, b| a.source_model.cmp(&b.source_model));
202 }
203 for list in belongs_to_of.values_mut() {
204 list.sort_by(|a, b| a.source_field.cmp(&b.source_field));
205 }
206
207 Self {
208 belongs_to,
209 has_many,
210 belongs_to_of,
211 }
212 }
213
214 pub fn belongs_to(&self, model: &str, field: &str) -> Option<&ResolvedRelation> {
216 self.belongs_to.get(&(model.to_string(), field.to_string()))
217 }
218
219 pub fn belongs_to_of(&self, model: &str) -> &[ResolvedRelation] {
221 self.belongs_to_of
222 .get(model)
223 .map(|v| v.as_slice())
224 .unwrap_or(&[])
225 }
226
227 pub fn has_many(&self, model: &str) -> &[InverseRelation] {
230 self.has_many
231 .get(model)
232 .map(|v| v.as_slice())
233 .unwrap_or(&[])
234 }
235
236 pub fn is_empty(&self) -> bool {
238 self.belongs_to.is_empty()
239 }
240
241 pub fn validate(&self, entries: &[AdminEntry]) -> Vec<RegistryError> {
244 let mut errors: Vec<RegistryError> = Vec::new();
245 let by_singular: HashMap<&str, &AdminEntry> =
246 entries.iter().map(|e| (e.singular_name, e)).collect();
247
248 for source in entries {
249 for field in source.fields {
250 let Some(rel) = &field.relation else {
251 continue;
252 };
253 let Some(target) = by_singular.get(rel.target_model) else {
254 errors.push(RegistryError::UnknownTarget {
255 model: source.singular_name.to_string(),
256 field: field.name.to_string(),
257 target: rel.target_model.to_string(),
258 });
259 continue;
260 };
261 if let Some(display) = rel.display_field {
262 if !target.fields.iter().any(|f| f.name == display) {
263 errors.push(RegistryError::UnknownDisplayField {
264 model: source.singular_name.to_string(),
265 field: field.name.to_string(),
266 target: rel.target_model.to_string(),
267 display: display.to_string(),
268 });
269 }
270 }
271 }
272 }
273
274 errors
275 }
276
277 pub fn iter_belongs_to(&self) -> impl Iterator<Item = &ResolvedRelation> {
280 let mut entries: Vec<&ResolvedRelation> = self.belongs_to.values().collect();
281 entries.sort_by(|a, b| {
282 a.source_model
283 .cmp(&b.source_model)
284 .then_with(|| a.source_field.cmp(&b.source_field))
285 });
286 entries.into_iter()
287 }
288}