diff --git a/rear/src/admin/domain.rs b/rear/src/admin/domain.rs index 2450603..f29d2f3 100644 --- a/rear/src/admin/domain.rs +++ b/rear/src/admin/domain.rs @@ -2,8 +2,8 @@ pub use config::AdminModelConfig; pub use dto::AdminApp; pub use dto::AdminModel; pub use repository::{ - AdminRepository, LookupKey, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem, - RepositoryList, Widget, + AdminRepository, DynAdminRepository, RepoInfo, RepositoryContext, RepositoryInfo, + RepositoryItem, RepositoryList, Widget, }; mod auth { @@ -54,6 +54,8 @@ pub mod repository { use async_trait::async_trait; use serde::{Serialize, Serializer}; use serde_json::Value; + use std::any::Any; + use std::fmt::Debug; use std::vec::IntoIter; pub type RepositoryContext = AdminModel; @@ -179,48 +181,6 @@ pub mod repository { } } - #[derive(PartialEq)] - pub enum LookupKey { - Integer(usize), - String(String), - } - - // Note that LookupKey auto converts to integer. - impl From for LookupKey { - fn from(s: String) -> Self { - if let Ok(int_key) = s.parse::() { - LookupKey::Integer(int_key) - } else { - LookupKey::String(s) - } - } - } - - impl From<&str> for LookupKey { - fn from(s: &str) -> Self { - if let Ok(int_key) = s.parse::() { - LookupKey::Integer(int_key) - } else { - LookupKey::String(s.to_owned()) - } - } - } - - impl From for LookupKey { - fn from(i: usize) -> Self { - LookupKey::Integer(i) - } - } - - impl std::fmt::Display for LookupKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LookupKey::Integer(i) => write!(f, "{}", i), - LookupKey::String(s) => write!(f, "{}", s), - } - } - } - #[derive(Serialize)] pub struct RepositoryItem { pub fields: Value, @@ -350,11 +310,41 @@ pub mod repository { } } + pub trait PrimaryKeyType: Any + Debug + Send + Sync { + fn as_any(&self) -> &dyn Any; + } + + impl PrimaryKeyType for i64 { + fn as_any(&self) -> &dyn Any { + self + } + } + + impl PrimaryKeyType for String { + fn as_any(&self) -> &dyn Any { + self + } + } + + impl From for Box { + fn from(s: String) -> Box { + if let Ok(i) = s.parse::() { + Box::new(i) + } else { + Box::new(s) + } + } + } + #[async_trait] pub trait AdminRepository: Send + Sync { + type Key: PrimaryKeyType; + + fn key_from_string(&self, s: String) -> Option; + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; async fn list(&self, context: &RepositoryContext) -> RepositoryList; - async fn get(&self, context: &RepositoryContext, id: LookupKey) -> Option; + async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option; async fn create( &mut self, context: &RepositoryContext, @@ -363,15 +353,140 @@ pub mod repository { async fn update( &mut self, context: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option; async fn replace( &mut self, context: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option; - async fn delete(&mut self, context: &RepositoryContext, id: LookupKey) -> Option; + async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option; + } + + #[async_trait] + pub trait DynAdminRepository: Send + Sync { + fn key_from_string(&self, s: String) -> Option>; + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; + async fn list(&self, context: &RepositoryContext) -> RepositoryList; + async fn get( + &self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option; + async fn create( + &mut self, + context: &RepositoryContext, + data: Value, + ) -> Option; + async fn update( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option; + async fn replace( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option; + async fn delete( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option; + } + + pub struct AdminRepositoryWrapper { + inner: T, + } + + impl AdminRepositoryWrapper { + pub fn new(inner: T) -> Self { + Self { inner } + } + + fn key_from_string(&self, s: String) -> Option<::Key> { + self.inner.key_from_string(s) + } + } + + #[async_trait] + impl DynAdminRepository for AdminRepositoryWrapper { + fn key_from_string(&self, s: String) -> Option> { + if let Some(key) = self.inner.key_from_string(s) { + Some(Box::new(key)) + } else { + None + } + } + + async fn info(&self, context: &RepositoryContext) -> RepositoryInfo { + self.inner.info(context).await + } + + async fn list(&self, context: &RepositoryContext) -> RepositoryList { + self.inner.list(context).await + } + + async fn get( + &self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.get(context, key).await + } else { + None + } + } + + async fn create( + &mut self, + context: &RepositoryContext, + data: Value, + ) -> Option { + self.inner.create(context, data).await + } + + async fn update( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.update(context, key, data).await + } else { + None + } + } + + async fn replace( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + data: Value, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.replace(context, key, data).await + } else { + None + } + } + + async fn delete( + &mut self, + context: &RepositoryContext, + id: &dyn PrimaryKeyType, + ) -> Option { + if let Some(key) = id.as_any().downcast_ref::() { + self.inner.delete(context, key).await + } else { + None + } + } } } diff --git a/rear/src/admin/state.rs b/rear/src/admin/state.rs index 5464eb7..6adf430 100644 --- a/rear/src/admin/state.rs +++ b/rear/src/admin/state.rs @@ -1,6 +1,7 @@ use tokio::sync::Mutex; -use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository}; +use super::domain::repository::AdminRepositoryWrapper; +use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository, DynAdminRepository}; use crate::service::templates::Templates; use std::collections::HashMap; use std::sync::Arc; @@ -17,7 +18,7 @@ pub struct AdminRegistry { base_path: String, apps: HashMap, models: HashMap, - repositories: HashMap>>, + repositories: HashMap>>, } impl AdminRegistry { @@ -113,6 +114,7 @@ impl AdminRegistry { repository: R, ) -> Result<(), String> { let model_key = self.register_model_config(model)?; + let repository = AdminRepositoryWrapper::new(repository); self.repositories .insert(model_key, Arc::new(Mutex::new(repository))); Ok(()) @@ -122,7 +124,7 @@ impl AdminRegistry { &self, app_key: &str, model_key: &str, - ) -> Result>, String> { + ) -> Result>, String> { let full_model_key = format!("{}.{}", app_key, model_key); if let Some(repo) = self.repositories.get(&full_model_key) { // Clone the Arc to return a reference to the repository diff --git a/rear/src/admin/views.rs b/rear/src/admin/views.rs index 2357653..2c05593 100644 --- a/rear/src/admin/views.rs +++ b/rear/src/admin/views.rs @@ -9,7 +9,7 @@ use crate::admin::domain::{AdminApp, AdminModel}; use crate::admin::state::AdminState; use serde::{Deserialize, Serialize}; -use super::domain::{LookupKey, RepositoryInfo, RepositoryItem, RepositoryList}; +use super::domain::{RepositoryInfo, RepositoryItem, RepositoryList}; #[derive(Serialize, Deserialize)] pub struct AdminRequest { @@ -157,15 +157,22 @@ pub async fn view_item_details( let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); - let key: LookupKey = id.into(); - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model).await), - item_list: repo.list(&admin_model).await, - item: repo.get(&admin_model, key).await, - item_model: Some(admin_model), - ..Default::default() + if let Some(key) = repo.key_from_string(id) { + AdminContext { + base: base_template(&headers), + available_apps: registry.get_apps(), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, + item: repo.get(&admin_model, key.as_ref()).await, + item_model: Some(admin_model), + ..Default::default() + } + } else { + AdminContext { + base: base_template(&headers), + available_apps: registry.get_apps(), + ..Default::default() + } } } else { AdminContext { @@ -257,15 +264,22 @@ pub async fn change_item( let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); - let key: LookupKey = id.into(); - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model).await), - item_list: repo.list(&admin_model).await, - item: repo.get(&admin_model, key).await, - item_model: Some(admin_model), - ..Default::default() + if let Some(key) = repo.key_from_string(id) { + AdminContext { + base: base_template(&headers), + available_apps: registry.get_apps(), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, + item: repo.get(&admin_model, key.as_ref()).await, + item_model: Some(admin_model), + ..Default::default() + } + } else { + AdminContext { + base: base_template(&headers), + available_apps: registry.get_apps(), + ..Default::default() + } } } else { AdminContext { @@ -290,18 +304,23 @@ pub async fn update_item( let admin_model = registry .get_model(&app_key, &model_key) .expect("Admin Model not found?"); - let key: LookupKey = id.into(); - - let result = repo.update(&admin_model, key, form).await; - - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - item_info: Some(repo.info(&admin_model).await), - item_list: repo.list(&admin_model).await, - item: result, - item_model: Some(admin_model), - ..Default::default() + if let Some(key) = repo.key_from_string(id) { + let result = repo.update(&admin_model, key.as_ref(), form).await; + AdminContext { + base: base_template(&headers), + available_apps: registry.get_apps(), + item_info: Some(repo.info(&admin_model).await), + item_list: repo.list(&admin_model).await, + item: result, + item_model: Some(admin_model), + ..Default::default() + } + } else { + AdminContext { + base: base_template(&headers), + available_apps: registry.get_apps(), + ..Default::default() + } } } else { AdminContext { diff --git a/src/admin_examples/empty_repository.rs b/src/admin_examples/empty_repository.rs index 114d1ec..91e6eed 100644 --- a/src/admin_examples/empty_repository.rs +++ b/src/admin_examples/empty_repository.rs @@ -8,6 +8,16 @@ impl Repository {} #[async_trait] impl AdminRepository for Repository { + type Key = i64; + + fn key_from_string(&self, s: String) -> Option { + if let Ok(i) = s.parse::() { + Some(i) + } else { + None + } + } + async fn info(&self, _: &RepositoryContext) -> RepositoryInfo { RepoInfo { name: "My Empty Repository", @@ -33,7 +43,7 @@ impl AdminRepository for Repository { } // GET single item. - async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option { + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { None } @@ -41,7 +51,7 @@ impl AdminRepository for Repository { async fn update( &mut self, model: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option { None @@ -51,14 +61,14 @@ impl AdminRepository for Repository { async fn replace( &mut self, model: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option { None } // DELETE single item. - async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option { + async fn delete(&mut self, _: &RepositoryContext, id: &Self::Key) -> Option { None } } diff --git a/src/admin_examples/static_repository.rs b/src/admin_examples/static_repository.rs index 1ac82d0..d62b32e 100644 --- a/src/admin_examples/static_repository.rs +++ b/src/admin_examples/static_repository.rs @@ -36,6 +36,16 @@ impl MyStaticRepository { #[async_trait] impl AdminRepository for MyStaticRepository { + type Key = i64; + + fn key_from_string(&self, s: String) -> Option { + if let Ok(i) = s.parse::() { + Some(i) + } else { + None + } + } + async fn info(&self, _: &RepositoryContext) -> RepositoryInfo { RepoInfo { name: "My Static Repository", @@ -48,15 +58,11 @@ impl AdminRepository for MyStaticRepository { //.set_widget("name", Widget::textarea().options(&[("disabled", "true")])) } - async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option { - if let LookupKey::Integer(id) = id { - let item = self.content.get(id - 1).cloned().unwrap(); - let id = item.get("id").unwrap(); - Some(model.build_item(&*id.to_string(), item)) - } else { - warn!("Got non-integer lookup key: {}", id); - None - } + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { + let id = *id as usize; + let item = self.content.get(id - 1).cloned().unwrap(); + let id = item.get("id").unwrap(); + Some(model.build_item(&*id.to_string(), item)) } async fn list(&self, model: &RepositoryContext) -> RepositoryList { @@ -94,7 +100,7 @@ impl AdminRepository for MyStaticRepository { async fn update( &mut self, model: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option { debug!("I would now update: {}, {}", id, data); @@ -102,10 +108,7 @@ impl AdminRepository for MyStaticRepository { // 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, - } + item_id == id } else { false } @@ -127,21 +130,18 @@ impl AdminRepository for MyStaticRepository { async fn replace( &mut self, model: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option { self.update(model, id, data).await } - async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option { + async fn delete(&mut self, _: &RepositoryContext, id: &Self::Key) -> Option { debug!("Would delete: {}", id); 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, - } + item_id == id } else { false } diff --git a/src/admin_examples/user_repository.rs b/src/admin_examples/user_repository.rs index 16a483c..19db3fd 100644 --- a/src/admin_examples/user_repository.rs +++ b/src/admin_examples/user_repository.rs @@ -19,6 +19,16 @@ impl UserRepository { #[async_trait] impl AdminRepository for UserRepository { + type Key = i64; + + fn key_from_string(&self, s: String) -> Option { + if let Ok(i) = s.parse::() { + Some(i) + } else { + None + } + } + async fn info(&self, _: &RepositoryContext) -> RepositoryInfo { RepoInfo { name: "User", @@ -29,25 +39,24 @@ impl AdminRepository for UserRepository { .into() } - async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option { - if let LookupKey::Integer(id) = id { - let id: i32 = id as i32; // use try_into() instead. - let get_user = entity::User::find_by_id(id).one(&self.connection).await; - match get_user { - Ok(get_user) => { - if let Some(user) = get_user { - let id = user.id.to_string(); - match serde_json::to_value(&user) { - Ok(item) => { - return Some(model.build_item(&*id, item)); - } - Err(_) => return None, + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { + let id: i32 = *id as i32; // use try_into() instead. + let get_user = entity::User::find_by_id(id).one(&self.connection).await; + match get_user { + Ok(get_user) => { + if let Some(user) = get_user { + let id = user.id.to_string(); + match serde_json::to_value(&user) { + Ok(item) => { + return Some(model.build_item(&*id, item)); } + Err(_) => return None, } } - Err(_) => return None, } + Err(_) => return None, } + None } @@ -94,58 +103,56 @@ impl AdminRepository for UserRepository { async fn update( &mut self, model: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option { - if let LookupKey::Integer(id) = id { - let id: i32 = id as i32; // use try_into() instead. - let user: Option = entity::User::find_by_id(id) - .one(&self.connection) - .await - .unwrap(); - let mut user: entity::user::ActiveModel = user.unwrap().into(); + let id: i32 = *id as i32; + let user: Option = entity::User::find_by_id(id) + .one(&self.connection) + .await + .unwrap(); + let mut user: entity::user::ActiveModel = user.unwrap().into(); - // change values - if let Some(value) = data.get("username") { - if let Some(value) = value.as_str() { - user.username = Set(value.to_owned()); - } - } - - if let Some(value) = data.get("description") { - user.description = Set(value.as_str().map(|s| s.to_owned())); - } - - // update - if let Ok(user) = user.update(&self.connection).await { - let id = user.id.to_string(); - return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap())); + // change values + if let Some(value) = data.get("username") { + if let Some(value) = value.as_str() { + user.username = Set(value.to_owned()); } } + + if let Some(value) = data.get("description") { + user.description = Set(value.as_str().map(|s| s.to_owned())); + } + + // update + if let Ok(user) = user.update(&self.connection).await { + let id = user.id.to_string(); + return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap())); + } + None } async fn replace( &mut self, model: &RepositoryContext, - id: LookupKey, + id: &Self::Key, data: Value, ) -> Option { self.update(model, id, data).await } - async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option { - if let LookupKey::Integer(id) = id { - let id: i32 = id as i32; // use try_into() instead. - let user: Option = entity::User::find_by_id(id) - .one(&self.connection) - .await - .unwrap(); - if let Some(user) = user { - let delete_result = user.delete(&self.connection).await.unwrap(); - debug!("deleted rows: {}", delete_result.rows_affected); - } + async fn delete(&mut self, _: &RepositoryContext, id: &Self::Key) -> Option { + let id: i32 = *id as i32; + let user: Option = entity::User::find_by_id(id) + .one(&self.connection) + .await + .unwrap(); + if let Some(user) = user { + let delete_result = user.delete(&self.connection).await.unwrap(); + debug!("deleted rows: {}", delete_result.rows_affected); } + None } } diff --git a/src/bin/generic_trait_type_erasure.rs b/src/bin/generic_trait_type_erasure.rs new file mode 100644 index 0000000..d6c9428 --- /dev/null +++ b/src/bin/generic_trait_type_erasure.rs @@ -0,0 +1,142 @@ +use std::any::Any; +use std::collections::HashMap; +use std::fmt::Debug; + +// Define the Key trait with Any and Debug for type erasure and debugging +pub trait Key: Any + Debug { + fn as_any(&self) -> &dyn Any; +} + +// Implement Key for i32 +impl Key for i32 { + fn as_any(&self) -> &dyn Any { + self + } +} + +// Implement Key for String +impl Key for String { + fn as_any(&self) -> &dyn Any { + self + } +} + +// Implement From for i32 +impl From for Box { + fn from(s: String) -> Box { + if let Ok(i) = s.parse::() { + Box::new(i) + } else { + Box::new(s) + } + } +} + +// Define the Repository trait with an associated type Key +pub trait Repository { + type Key: Key; + fn get_entry(&self, key: &Self::Key) -> String; +} + +// Define a helper trait for dynamic dispatch +pub trait DynRepository { + fn get_entry(&self, key: &dyn Key) -> String; +} + +// Define a type-erased wrapper for Repository +pub struct RepositoryWrapper { + inner: T, +} + +impl RepositoryWrapper { + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +impl DynRepository for RepositoryWrapper { + fn get_entry(&self, key: &dyn Key) -> String { + if let Some(key) = key.as_any().downcast_ref::() { + self.inner.get_entry(key) + } else { + "Invalid Key Type".to_string() + } + } +} + +// Implement the Repository trait for IntRepository +pub struct IntRepository { + data: HashMap, +} + +impl IntRepository { + pub fn new() -> Self { + let mut data = HashMap::new(); + data.insert(1, "IntEntry1".to_string()); + data.insert(2, "IntEntry2".to_string()); + Self { data } + } +} + +impl Repository for IntRepository { + type Key = i32; + + fn get_entry(&self, key: &Self::Key) -> String { + self.data + .get(key) + .unwrap_or(&"Not Found".to_string()) + .clone() + } +} + +// Implement the Repository trait for StringRepository +pub struct StringRepository { + data: HashMap, +} + +impl StringRepository { + pub fn new() -> Self { + let mut data = HashMap::new(); + data.insert("one".to_string(), "StringEntry1".to_string()); + data.insert("two".to_string(), "StringEntry2".to_string()); + Self { data } + } +} + +impl Repository for StringRepository { + type Key = String; + + fn get_entry(&self, key: &Self::Key) -> String { + self.data + .get(key) + .unwrap_or(&"Not Found".to_string()) + .clone() + } +} + +fn main() { + let int_repo = IntRepository::new(); + let string_repo = StringRepository::new(); + + let repositories: Vec> = vec![ + Box::new(RepositoryWrapper::new(int_repo)), + Box::new(RepositoryWrapper::new(string_repo)), + ]; + + // Creating keys from strings using the From implementation + let keys: Vec> = vec![ + Box::new(1_i32), + Box::new("one".to_string()), + Box::new(99_i32), // non-existing int key + Box::new("unknown".to_string()), // non-existing string key + Box::from("123".to_string()), // i32 key from string + Box::from("hello".to_string()), // String key from string + ]; + + for key in keys { + for repo in &repositories { + let result = repo.get_entry(key.as_ref()); + println!("Result: {}", result); + } + } +}