autumn_admin_plugin/
registry.rs1use std::collections::btree_map::{BTreeMap, Entry};
8
9use crate::traits::AdminModel;
10
11const RESERVED_MODEL_SLUGS: &[&str] = &["jobs"];
16
17pub struct AdminRegistry {
22 models: BTreeMap<&'static str, Box<dyn AdminModel>>,
24}
25
26impl AdminRegistry {
27 pub(crate) fn new() -> Self {
29 Self {
30 models: BTreeMap::new(),
31 }
32 }
33
34 pub(crate) fn register<M: AdminModel>(&mut self, model: M) {
37 let slug = model.slug();
38 assert!(
39 !RESERVED_MODEL_SLUGS.contains(&slug),
40 "autumn-admin: reserved model slug '{slug}' conflicts with built-in admin routes",
41 );
42 let name = model.display_name();
43 match self.models.entry(slug) {
44 Entry::Occupied(_) => panic!(
45 "autumn-admin: duplicate model slug '{slug}' — each model must have a unique slug",
46 ),
47 Entry::Vacant(e) => {
48 tracing::debug!(slug, name, "Registered admin model");
49 e.insert(Box::new(model));
50 }
51 }
52 }
53
54 #[must_use]
56 pub fn model_count(&self) -> usize {
57 self.models.len()
58 }
59
60 #[must_use]
62 pub fn get(&self, slug: &str) -> Option<&dyn AdminModel> {
63 self.models.get(slug).map(Box::as_ref)
64 }
65
66 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &dyn AdminModel)> {
68 self.models
69 .iter()
70 .map(|(&slug, model)| (slug, model.as_ref()))
71 }
72
73 #[must_use]
75 pub fn slugs(&self) -> Vec<&'static str> {
76 self.models.keys().copied().collect()
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::traits::*;
84 use serde_json::Value;
85
86 struct DummyModel {
87 slug: &'static str,
88 name: &'static str,
89 }
90
91 impl AdminModel for DummyModel {
92 fn slug(&self) -> &'static str {
93 self.slug
94 }
95 fn display_name(&self) -> &'static str {
96 self.name
97 }
98 fn display_name_plural(&self) -> &'static str {
99 self.name
100 }
101 fn fields(&self) -> Vec<AdminField> {
102 vec![]
103 }
104 fn list(
105 &self,
106 _pool: &diesel_async::pooled_connection::deadpool::Pool<
107 diesel_async::AsyncPgConnection,
108 >,
109 _params: ListParams,
110 ) -> AdminFuture<'_, ListResult> {
111 Box::pin(async {
112 Ok(ListResult {
113 records: vec![],
114 total: 0,
115 page: 1,
116 per_page: 25,
117 })
118 })
119 }
120 fn get(
121 &self,
122 _pool: &diesel_async::pooled_connection::deadpool::Pool<
123 diesel_async::AsyncPgConnection,
124 >,
125 _id: i64,
126 ) -> AdminFuture<'_, Option<Value>> {
127 Box::pin(async { Ok(None) })
128 }
129 fn create(
130 &self,
131 _pool: &diesel_async::pooled_connection::deadpool::Pool<
132 diesel_async::AsyncPgConnection,
133 >,
134 data: Value,
135 ) -> AdminFuture<'_, Value> {
136 Box::pin(async move { Ok(data) })
137 }
138 fn update(
139 &self,
140 _pool: &diesel_async::pooled_connection::deadpool::Pool<
141 diesel_async::AsyncPgConnection,
142 >,
143 _id: i64,
144 data: Value,
145 ) -> AdminFuture<'_, Value> {
146 Box::pin(async move { Ok(data) })
147 }
148 fn delete(
149 &self,
150 _pool: &diesel_async::pooled_connection::deadpool::Pool<
151 diesel_async::AsyncPgConnection,
152 >,
153 _id: i64,
154 ) -> AdminFuture<'_, ()> {
155 Box::pin(async { Ok(()) })
156 }
157 }
158
159 #[test]
160 fn register_and_retrieve() {
161 let mut registry = AdminRegistry::new();
162 registry.register(DummyModel {
163 slug: "projects",
164 name: "Project",
165 });
166 assert_eq!(registry.model_count(), 1);
167 assert!(registry.get("projects").is_some());
168 assert!(registry.get("nonexistent").is_none());
169 }
170
171 #[test]
172 fn iter_is_sorted() {
173 let mut registry = AdminRegistry::new();
174 registry.register(DummyModel {
175 slug: "tickets",
176 name: "Ticket",
177 });
178 registry.register(DummyModel {
179 slug: "projects",
180 name: "Project",
181 });
182 let slugs: Vec<_> = registry.iter().map(|(s, _)| s).collect();
183 assert_eq!(slugs, vec!["projects", "tickets"]);
184 }
185
186 #[test]
187 #[should_panic(expected = "duplicate model slug")]
188 fn duplicate_slug_panics() {
189 let mut registry = AdminRegistry::new();
190 registry.register(DummyModel {
191 slug: "projects",
192 name: "Project",
193 });
194 registry.register(DummyModel {
195 slug: "projects",
196 name: "Project 2",
197 });
198 }
199
200 #[test]
201 #[should_panic(expected = "reserved model slug 'jobs'")]
202 fn reserved_jobs_slug_panics() {
203 let mut registry = AdminRegistry::new();
204 registry.register(DummyModel {
205 slug: "jobs",
206 name: "Jobs",
207 });
208 }
209
210 #[test]
211 fn config_slug_allowed_without_runtime_config() {
212 let mut registry = AdminRegistry::new();
215 registry.register(DummyModel {
216 slug: "config",
217 name: "Config",
218 });
219 assert_eq!(registry.model_count(), 1);
220 }
221}