From 0e6649de1b3f7c485941d1d3c33cee8270d4c81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Sun, 28 Jan 2024 18:11:02 +0100 Subject: [PATCH] code: functioning example for repository registry; using tokio::sync::Mutex for read/write lock; prototype create --- TODOS.md | 11 ++-- src/admin/domain.rs | 16 ++++- src/admin/example.rs | 86 +++++++++++++++++++++---- src/admin/state.rs | 12 ++-- src/admin/views.rs | 4 ++ templates/admin/items/item_create.jinja | 29 ++++++--- 6 files changed, 126 insertions(+), 32 deletions(-) diff --git a/TODOS.md b/TODOS.md index 8f7eaa7..8f6552d 100644 --- a/TODOS.md +++ b/TODOS.md @@ -17,13 +17,16 @@ edit functionality: -> how would i create widgets? programmatically? in templates? -> maybe even both approaches available? -> need a list of MVP widgets, and a way to extend them. + -> widgets should be groupable. + -> taking a look at djangos solution! + -> are there any smart json data => form things out there? - validation. - -> client side as well? + -> client side as well? fomantic has it. -> existing solutions? -> transparent way to display error messages at the appropriate field (probably adding results into responses) - -> required - -> invalid - -> invalid(max-length) + -> handling: "required" + -> handling: "invalid" + -> example implementation: invalid(max-length) auth: - user model. diff --git a/src/admin/domain.rs b/src/admin/domain.rs index 33c7bd8..f2b3ff9 100644 --- a/src/admin/domain.rs +++ b/src/admin/domain.rs @@ -56,6 +56,7 @@ pub mod repository { use serde_json::Value; use std::vec::IntoIter; + #[derive(PartialEq)] pub enum LookupKey { Integer(usize), String(String), @@ -152,6 +153,7 @@ pub mod repository { pub name: &'static str, pub lookup_key: &'static str, pub display_list: &'static [&'static str], + pub fields: &'static [&'static str], } // consider builder: https://docs.rs/derive_builder/latest/derive_builder/ @@ -163,6 +165,7 @@ pub mod repository { name: String, lookup_key: String, display_list: Vec, + fields: Vec, } impl RepositoryInfo { @@ -171,6 +174,7 @@ pub mod repository { name: name.to_owned(), lookup_key: lookup_key.to_owned(), display_list: vec![], + fields: vec![], } } @@ -192,6 +196,7 @@ pub mod repository { .iter() .map(|&s| s.to_string()) .collect(), + fields: repo_info.fields.iter().map(|&s| s.to_string()).collect(), } } } @@ -200,8 +205,13 @@ pub mod repository { fn info(&self, model: &AdminModel) -> RepositoryInfo; fn list(&self, model: &AdminModel) -> RepositoryList; fn get(&self, model: &AdminModel, id: LookupKey) -> Option; - fn create(&self, model: &AdminModel, data: Value) -> Option; - fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option; - fn delete(&self, model: &AdminModel, id: LookupKey) -> Option; + fn create(&mut self, model: &AdminModel, data: Value) -> Option; + fn update( + &mut self, + model: &AdminModel, + id: LookupKey, + data: Value, + ) -> Option; + fn delete(&mut self, model: &AdminModel, id: LookupKey) -> Option; } } diff --git a/src/admin/example.rs b/src/admin/example.rs index 202ca24..65dfcdc 100644 --- a/src/admin/example.rs +++ b/src/admin/example.rs @@ -2,16 +2,20 @@ use super::domain::*; use super::state::AdminRegistry; -use log::warn; +use log::{debug, warn}; use serde_json::{json, Value}; struct MyStaticRepository { + /// In Memory Storage for Example. content: Vec, + /// ID Counter for content. + next_id: usize, } impl MyStaticRepository { pub fn new() -> Self { MyStaticRepository { + next_id: 12, content: vec![ json!({"id": 1, "name": "Strange", "age": 150, "level": "master" }), json!({"id": 2, "name": "Adam", "age": 12, "powers": 8}), @@ -48,7 +52,6 @@ impl AdminRepository for MyStaticRepository { } fn list(&self, model: &AdminModel) -> RepositoryList { - // Admin needs to inject info in these. RepositoryList::List { values: self .content @@ -66,28 +69,87 @@ impl AdminRepository for MyStaticRepository { } } - fn info(&self, model: &AdminModel) -> RepositoryInfo { + fn info(&self, _model: &AdminModel) -> RepositoryInfo { RepoInfo { name: "My Static Repository", lookup_key: "id", display_list: &["id", "name", "level", "age"], + fields: &["name", "level", "age", "powers"], } .into() } - fn create(&self, model: &AdminModel, data: Value) -> Option { - println!("I would now create: {}", data); + fn create(&mut self, model: &AdminModel, mut data: Value) -> Option { + debug!("Asked to create: {}", data); + + let new_id = self.next_id; + self.next_id += 1; + + data["id"] = Value::Number(new_id.into()); + + debug!("I create: {}", data); + + // Push the data into the repository + self.content.push(data.clone()); + + // Return the newly created item + Some(RepositoryItem { + detail_url: Some(format!("{}/detail/{}", model.admin_url, new_id)), + fields: data, + }) + } + + fn update(&mut self, model: &AdminModel, id: LookupKey, data: Value) -> Option { + debug!("I would now update: {}, {}", id, data); + + // First, find the index of the item to update + let item_index = self.content.iter().position(|item| { + if let Some(item_id) = item.get("id") { + match id { + LookupKey::Integer(i) => item_id == &i, + LookupKey::String(ref s) => item_id == s, + } + } else { + false + } + }); + + // Then, update the item if found + if let Some(index) = item_index { + let item = &mut self.content[index]; + *item = data.clone(); + + if let Some(item_id) = item.get("id") { + return Some(RepositoryItem { + detail_url: Some(format!("{}/detail/{}", model.admin_url, item_id)), + fields: data, + }); + } + } + None } - fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option { - println!("I would now update: {}, {}", id, data); - None - } + fn delete(&mut self, _model: &AdminModel, id: LookupKey) -> Option { + debug!("Would delete: {}", id); - fn delete(&self, model: &AdminModel, id: LookupKey) -> Option { - println!("Would delete: {}", id); - None + let item_index = self.content.iter().position(|item| { + if let Some(item_id) = item.get("id") { + match id { + LookupKey::Integer(i) => item_id == &i, + LookupKey::String(ref s) => item_id == s, + } + } else { + false + } + }); + + if let Some(index) = item_index { + // Remove and return the item + Some(self.content.remove(index)) + } else { + None + } } } diff --git a/src/admin/state.rs b/src/admin/state.rs index 36ef52f..a9301c1 100644 --- a/src/admin/state.rs +++ b/src/admin/state.rs @@ -1,3 +1,5 @@ +use tokio::sync::Mutex; + use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository}; use std::collections::HashMap; use std::sync::Arc; @@ -9,7 +11,7 @@ pub struct AdminRegistry { base_path: String, apps: HashMap, models: HashMap, - repositories: HashMap>, + repositories: HashMap>>, } impl AdminRegistry { @@ -105,7 +107,8 @@ impl AdminRegistry { repository: R, ) -> Result<(), String> { let model_key = self.register_model_config(model)?; - self.repositories.insert(model_key, Box::new(repository)); + self.repositories + .insert(model_key, Arc::new(Mutex::new(repository))); Ok(()) } @@ -113,10 +116,11 @@ impl AdminRegistry { &self, app_key: &str, model_key: &str, - ) -> Result<&Box, String> { + ) -> Result>, String> { let full_model_key = format!("{}.{}", app_key, model_key); if let Some(repo) = self.repositories.get(&full_model_key) { - return Ok(repo); + // Clone the Arc to return a reference to the repository + return Ok(Arc::clone(repo)); } else { return Err("Couldn't find repository".to_owned()); } diff --git a/src/admin/views.rs b/src/admin/views.rs index efb372f..2e4a970 100644 --- a/src/admin/views.rs +++ b/src/admin/views.rs @@ -114,6 +114,7 @@ pub async fn list_item_collection( info!("list_item_collection {} for model {}", app_key, model_key); let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { + let repo = repo.lock().await; let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist @@ -152,6 +153,7 @@ pub async fn item_details( Path((app_key, model_key, id)): Path<(String, String, String)>, ) -> impl IntoResponse { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { + let repo = repo.lock().await; let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist @@ -184,6 +186,7 @@ pub async fn new_item( Path((app_key, model_key)): Path<(String, String)>, ) -> impl IntoResponse { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { + let repo = repo.lock().await; let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); @@ -213,6 +216,7 @@ pub async fn create_item( Form(form): Form, ) -> impl IntoResponse { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { + let mut repo = repo.lock().await; let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); diff --git a/templates/admin/items/item_create.jinja b/templates/admin/items/item_create.jinja index d0e5427..101a5bb 100644 --- a/templates/admin/items/item_create.jinja +++ b/templates/admin/items/item_create.jinja @@ -1,17 +1,28 @@ {% extends base|none("admin/base.jinja") %} {% macro input(name, value="", type="text") -%} - + {%- endmacro %} {% block content %} -Create {{item_model.name}} in {{item_info.name}} +
+

Create {{item_model.name}} in {{item_info.name}}

-
- {% set fields = item_info.display_list %} - {% for field in fields %} -

{{ input(field) }}

- {% endfor %} - -
+
+ {% set fields = item_info.fields %} + {% for field in fields %} + {% if item %} + {% set field_value = item.fields[field]|none("") %} + {% else %} + {% set field_value = "" %} + {% endif %} + +
+ + {{ input(field, field_value) }} +
+ {% endfor %} + +
+
{% endblock content %} \ No newline at end of file