1use crate::error::AdminError;
10use crate::model::ModelDefinition;
11use async_trait::async_trait;
12use parking_lot::RwLock;
13use serde_json::Value;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, Default)]
23pub struct DataQuery {
24 pub offset: usize,
26 pub limit: usize,
33 pub order_by: Vec<String>,
35 pub search: Option<String>,
37 pub filters: HashMap<String, String>,
39}
40
41#[derive(Debug, Clone, Default)]
43pub struct DataPage {
44 pub rows: Vec<Value>,
46 pub total: usize,
48}
49
50#[async_trait]
55pub trait DataSource: Send + Sync {
56 async fn list(&self, model: &ModelDefinition, query: &DataQuery) -> DataPage;
65
66 async fn get(&self, model: &ModelDefinition, id: &str) -> Option<Value>;
68
69 async fn count(&self, model: &ModelDefinition) -> usize;
71
72 async fn create(&self, model: &ModelDefinition, data: Value) -> Result<String, AdminError>;
74
75 async fn update(
77 &self,
78 model: &ModelDefinition,
79 id: &str,
80 data: Value,
81 ) -> Result<(), AdminError>;
82
83 async fn delete(&self, model: &ModelDefinition, id: &str) -> Result<(), AdminError>;
85}
86
87#[derive(Default)]
93pub struct InMemoryDataSource {
94 tables: RwLock<HashMap<String, Vec<Value>>>,
95}
96
97impl InMemoryDataSource {
98 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn seed(&self, model_name: impl Into<String>, mut row: Value) {
106 let model_name = model_name.into();
107 if let Value::Object(map) = &mut row
108 && !map.contains_key("id")
109 {
110 map.insert(
111 "id".to_string(),
112 Value::String(uuid::Uuid::new_v4().to_string()),
113 );
114 }
115 self.tables.write().entry(model_name).or_default().push(row);
116 }
117
118 pub fn len(&self, model_name: &str) -> usize {
120 self.tables
121 .read()
122 .get(model_name)
123 .map(|v| v.len())
124 .unwrap_or(0)
125 }
126
127 fn pk_of(model: &ModelDefinition, row: &Value) -> Option<String> {
128 row.get(&model.primary_key).map(value_to_plain_string)
129 }
130}
131
132#[async_trait]
133impl DataSource for InMemoryDataSource {
134 async fn list(&self, model: &ModelDefinition, query: &DataQuery) -> DataPage {
135 let tables = self.tables.read();
136 let all = match tables.get(&model.name) {
137 Some(rows) => rows,
138 None => return DataPage::default(),
139 };
140
141 let mut matched: Vec<Value> = all
143 .iter()
144 .filter(|row| {
145 query.filters.iter().all(|(field, want)| {
146 row.get(field)
147 .map(|v| value_to_plain_string(v) == *want)
148 .unwrap_or(false)
149 })
150 })
151 .filter(|row| match &query.search {
152 None => true,
153 Some(needle) => {
154 let needle = needle.to_lowercase();
155 model.search_fields.iter().any(|field| {
156 row.get(field)
157 .map(|v| value_to_plain_string(v).to_lowercase().contains(&needle))
158 .unwrap_or(false)
159 })
160 }
161 })
162 .cloned()
163 .collect();
164
165 if let Some(first) = query.order_by.first() {
167 let mut parts = first.split_whitespace();
168 if let Some(field) = parts.next() {
169 let descending = parts.next().map(|d| d.eq_ignore_ascii_case("DESC")) == Some(true);
170 matched.sort_by(|a, b| {
171 let av = a.get(field).map(value_to_plain_string).unwrap_or_default();
172 let bv = b.get(field).map(value_to_plain_string).unwrap_or_default();
173 if descending { bv.cmp(&av) } else { av.cmp(&bv) }
174 });
175 }
176 }
177
178 let total = matched.len();
179 let rows = if query.limit == 0 {
180 matched.into_iter().skip(query.offset).collect()
181 } else {
182 matched
183 .into_iter()
184 .skip(query.offset)
185 .take(query.limit)
186 .collect()
187 };
188 DataPage { rows, total }
189 }
190
191 async fn get(&self, model: &ModelDefinition, id: &str) -> Option<Value> {
192 let tables = self.tables.read();
193 tables.get(&model.name).and_then(|rows| {
194 rows.iter()
195 .find(|row| Self::pk_of(model, row).as_deref() == Some(id))
196 .cloned()
197 })
198 }
199
200 async fn count(&self, model: &ModelDefinition) -> usize {
201 self.len(&model.name)
202 }
203
204 async fn create(&self, model: &ModelDefinition, mut data: Value) -> Result<String, AdminError> {
205 let map = data
206 .as_object_mut()
207 .ok_or_else(|| AdminError::Validation("record must be a JSON object".to_string()))?;
208
209 let id = match map.get(&model.primary_key) {
210 Some(v) if !v.is_null() => value_to_plain_string(v),
211 _ => {
212 let id = uuid::Uuid::new_v4().to_string();
213 map.insert(model.primary_key.clone(), Value::String(id.clone()));
214 id
215 }
216 };
217
218 self.tables
219 .write()
220 .entry(model.name.clone())
221 .or_default()
222 .push(data);
223 Ok(id)
224 }
225
226 async fn update(
227 &self,
228 model: &ModelDefinition,
229 id: &str,
230 data: Value,
231 ) -> Result<(), AdminError> {
232 let mut tables = self.tables.write();
233 let rows = tables
234 .get_mut(&model.name)
235 .ok_or_else(|| AdminError::RecordNotFound {
236 model: model.name.clone(),
237 id: id.to_string(),
238 })?;
239
240 let slot = rows
241 .iter_mut()
242 .find(|row| Self::pk_of(model, row).as_deref() == Some(id))
243 .ok_or_else(|| AdminError::RecordNotFound {
244 model: model.name.clone(),
245 id: id.to_string(),
246 })?;
247
248 if let (Some(existing), Some(incoming)) = (slot.as_object_mut(), data.as_object()) {
250 for (k, v) in incoming {
251 if k == &model.primary_key {
252 continue;
253 }
254 existing.insert(k.clone(), v.clone());
255 }
256 } else {
257 *slot = data;
258 }
259 Ok(())
260 }
261
262 async fn delete(&self, model: &ModelDefinition, id: &str) -> Result<(), AdminError> {
263 let mut tables = self.tables.write();
264 let rows = tables
265 .get_mut(&model.name)
266 .ok_or_else(|| AdminError::RecordNotFound {
267 model: model.name.clone(),
268 id: id.to_string(),
269 })?;
270
271 let before = rows.len();
272 rows.retain(|row| Self::pk_of(model, row).as_deref() != Some(id));
273 if rows.len() == before {
274 return Err(AdminError::RecordNotFound {
275 model: model.name.clone(),
276 id: id.to_string(),
277 });
278 }
279 Ok(())
280 }
281}
282
283pub(crate) fn value_to_plain_string(value: &Value) -> String {
285 match value {
286 Value::String(s) => s.clone(),
287 Value::Null => String::new(),
288 other => other.to_string(),
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::field::{FieldDefinition, FieldType};
296
297 fn user_model() -> ModelDefinition {
298 ModelDefinition::builder("user")
299 .id_field()
300 .field(FieldDefinition::new("name", FieldType::String).searchable())
301 .search_fields(["name"])
302 .list_display(["id", "name"])
303 .build()
304 }
305
306 #[tokio::test]
307 async fn stub_list_paginates_and_totals() {
308 let ds = InMemoryDataSource::new();
309 let model = user_model();
310 for i in 0..5 {
311 ds.seed(
312 "user",
313 serde_json::json!({ "id": i, "name": format!("u{i}") }),
314 );
315 }
316
317 let page = ds
318 .list(
319 &model,
320 &DataQuery {
321 offset: 0,
322 limit: 2,
323 ..Default::default()
324 },
325 )
326 .await;
327
328 assert_eq!(page.rows.len(), 2, "limit must cap returned rows");
329 assert_eq!(page.total, 5, "total must ignore pagination");
330 }
331
332 #[tokio::test]
333 async fn stub_crud_roundtrip() {
334 let ds = InMemoryDataSource::new();
335 let model = user_model();
336
337 let id = ds
338 .create(&model, serde_json::json!({ "id": "7", "name": "Alice" }))
339 .await
340 .unwrap();
341 assert_eq!(id, "7");
342 assert_eq!(ds.get(&model, "7").await.unwrap()["name"], "Alice");
343
344 ds.update(&model, "7", serde_json::json!({ "name": "Bob" }))
345 .await
346 .unwrap();
347 assert_eq!(ds.get(&model, "7").await.unwrap()["name"], "Bob");
348
349 ds.delete(&model, "7").await.unwrap();
350 assert!(ds.get(&model, "7").await.is_none());
351 }
352
353 #[tokio::test]
354 async fn stub_search_filters_rows() {
355 let ds = InMemoryDataSource::new();
356 let model = user_model();
357 ds.seed("user", serde_json::json!({ "id": 1, "name": "Alice" }));
358 ds.seed("user", serde_json::json!({ "id": 2, "name": "Bob" }));
359
360 let page = ds
361 .list(
362 &model,
363 &DataQuery {
364 search: Some("ali".to_string()),
365 ..Default::default()
366 },
367 )
368 .await;
369 assert_eq!(page.total, 1);
370 assert_eq!(page.rows[0]["name"], "Alice");
371 }
372}