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