From 843e432ec4aca89a30f605e4f8b4113ff6f54d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Tue, 23 Jul 2024 00:18:04 +0200 Subject: [PATCH] refactor: result type, removing rear.admin --- Cargo.lock | 17 +- rear/Cargo.toml | 1 + rear/src/admin/domain.rs | 486 ------------------------ rear/src/admin/mod.rs | 27 -- rear/src/admin/state.rs | 174 --------- rear/src/admin/views.rs | 350 ----------------- rear/src/auth/models.rs | 4 - rear/src/depot/mod.rs | 10 +- rear/src/depot/prelude.rs | 4 +- rear/src/depot/registry.rs | 6 +- rear/src/depot/repository.rs | 65 ++-- rear/src/depot/views.rs | 14 +- rear_auth/src/user_admin_repository.rs | 43 ++- src/admin_examples/empty_repository.rs | 20 +- src/admin_examples/file_repository.rs | 20 +- src/admin_examples/static_repository.rs | 26 +- 16 files changed, 127 insertions(+), 1140 deletions(-) delete mode 100644 rear/src/admin/domain.rs delete mode 100644 rear/src/admin/mod.rs delete mode 100644 rear/src/admin/state.rs delete mode 100644 rear/src/admin/views.rs delete mode 100644 rear/src/auth/models.rs diff --git a/Cargo.lock b/Cargo.lock index 319e3a0..ec2781b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2005,6 +2005,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -2464,6 +2470,7 @@ dependencies = [ "serde", "serde_json", "slug", + "thiserror", "tokio", "tracing", ] @@ -3459,12 +3466,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -3479,10 +3487,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] diff --git a/rear/Cargo.toml b/rear/Cargo.toml index 68e6318..854d2b5 100644 --- a/rear/Cargo.toml +++ b/rear/Cargo.toml @@ -26,3 +26,4 @@ serde_json = "1.0.108" slug = "0.1.5" async-trait = "0.1.77" tracing = "0.1.40" +thiserror = "1.0.61" diff --git a/rear/src/admin/domain.rs b/rear/src/admin/domain.rs deleted file mode 100644 index ba695b3..0000000 --- a/rear/src/admin/domain.rs +++ /dev/null @@ -1,486 +0,0 @@ -pub use config::AdminModelConfig; -pub use dto::AdminApp; -pub use dto::AdminModel; -pub use repository::{ - AdminRepository, DynAdminRepository, RepoInfo, RepositoryContext, RepositoryInfo, - RepositoryItem, RepositoryList, Widget, -}; - -mod auth { - - struct AdminUser {} - - struct AdminRole {} - - struct AdminGroup {} - - struct AdminActionLog {} -} - -mod config { - // user uses this configuration object to register another model. - pub struct AdminModelConfig { - pub name: String, - pub app_key: String, - } -} - -mod dto { - use serde::{Deserialize, Serialize}; - - #[derive(Deserialize, Serialize)] - pub struct AdminModel { - pub key: String, - pub name: String, - - pub admin_url: String, - pub view_only: bool, - - pub add_url: Option, - } - - #[derive(Deserialize, Serialize)] - pub struct AdminApp { - pub key: String, - pub name: String, - - pub app_url: String, - pub models: Vec, - } -} - -pub mod repository { - use super::dto::AdminModel; - 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; - - impl RepositoryContext { - pub fn get_default_detail_url(&self, key: &str) -> Option { - Some(format!("{}/detail/{}", self.admin_url, key)) - } - - pub fn get_default_change_url(&self, key: &str) -> Option { - Some(format!("{}/change/{}", self.admin_url, key)) - } - - pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem { - RepositoryItem { - detail_url: self.get_default_detail_url(key), - change_url: self.get_default_change_url(key), - fields: fields, - } - } - } - - /// This is a static configuration object. - /// It might be changed in the future to have a dynamic counterpart. - /// - /// ## Example: - /// Creating a simple required and readonly text input field with a label: - /// - /// ```ignore - /// let my_field = Field::widget("/admin/widgets/input_text.jinja") - /// .labelled("Username") - /// .required() - /// .readonly(); - /// ``` - #[derive(Debug, Serialize, Clone, Copy)] - pub struct Widget { - pub widget: &'static str, - pub label: Option<&'static str>, - #[serde(rename = "type")] - pub field_type: &'static str, - pub required: bool, - pub readonly: bool, - pub options: &'static [(&'static str, &'static str)], - } - - impl Widget { - pub fn widget(widget: &'static str) -> Self { - Widget { - widget: widget, - label: None, - field_type: "text", - required: false, - readonly: false, - options: &[], - } - } - - pub fn default() -> Self { - Self::widget("/admin/widgets/input_text.jinja") - } - - pub fn textarea() -> Self { - Self::widget("/admin/widgets/input_textarea.jinja") - } - - pub fn checkbox() -> Self { - Self::widget("/admin/widgets/checkbox_toggle.jinja") - } - - pub fn required(mut self) -> Self { - self.required = true; - self - } - - pub fn readonly(mut self) -> Self { - self.readonly = true; - self - } - - pub fn labeled(mut self, label: &'static str) -> Self { - self.label = Some(label); - self - } - - pub fn as_password(mut self) -> Self { - self.field_type = "password"; - self - } - - pub fn as_hidden(mut self) -> Self { - self.field_type = "hidden"; - self - } - - pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self { - self.options = options; - self - } - } - - #[derive(Debug, Serialize)] - pub struct Field { - widget: String, - label: Option, - #[serde(rename = "type")] - field_type: String, - readonly: bool, - required: bool, - options: Value, - } - - impl From for Field { - fn from(value: Widget) -> Self { - Field { - widget: value.widget.to_string(), - label: value.label.map(|s| s.to_string()), - field_type: value.field_type.to_string(), - readonly: value.readonly, - required: value.required, - options: value - .options - .iter() - .map(|(key, val)| (key.to_string(), serde_json::json!(val))) - .collect::>() - .into(), - } - } - } - - #[derive(Serialize)] - pub struct RepositoryItem { - pub fields: Value, - pub detail_url: Option, - pub change_url: Option, - } - - pub enum RepositoryList { - Empty, - List { - values: Vec, - }, - Page { - values: Vec, - offset: usize, - total: usize, - }, - Stream { - values: Vec, - next_index: Option, - }, - } - - impl IntoIterator for RepositoryList { - type Item = RepositoryItem; - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - RepositoryList::Empty => vec![].into_iter(), - RepositoryList::List { values } => values.into_iter(), - RepositoryList::Page { values, .. } => values.into_iter(), - RepositoryList::Stream { values, .. } => values.into_iter(), - } - } - } - - impl Serialize for RepositoryList { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - RepositoryList::Empty => serializer.serialize_unit(), - RepositoryList::List { values } - | RepositoryList::Page { values, .. } - | RepositoryList::Stream { values, .. } => values.serialize(serializer), - } - } - } - - /// Static initializer for RepositoryInfo. - pub struct RepoInfo { - pub name: &'static str, - pub lookup_key: &'static str, - pub display_list: &'static [&'static str], - pub fields: &'static [&'static str], - } - - impl RepoInfo { - pub fn build(self) -> RepositoryInfo { - self.into() - } - } - - #[derive(Serialize)] - pub struct RepositoryInfo { - name: String, - lookup_key: String, - display_list: Vec, - fields: Vec<(String, Field)>, - } - - impl RepositoryInfo { - pub fn new(name: &str, lookup_key: &str) -> Self { - RepositoryInfo { - name: name.to_owned(), - lookup_key: lookup_key.to_owned(), - display_list: vec![], - fields: vec![], - } - } - - // self mutating builder pattern - pub fn display_list(mut self, display_list: &[&str]) -> Self { - self.display_list = display_list.iter().map(|&e| e.to_string()).collect(); - self - } - - pub fn set_widget>(mut self, name: &str, item: T) -> Self { - let field = item.into(); // Convert the input into a Field - - // Find the index of the existing entry with the same name, if it exists - let pos = self - .fields - .iter() - .position(|(existing_name, _)| existing_name == name); - - match pos { - Some(index) => { - self.fields[index].1 = field; - } - None => { - self.fields.push((name.to_owned(), field)); - } - } - self - } - } - - impl From for RepositoryInfo { - fn from(repo_info: RepoInfo) -> Self { - RepositoryInfo { - name: repo_info.name.to_string(), - lookup_key: repo_info.lookup_key.to_string(), - display_list: repo_info - .display_list - .iter() - .map(|&s| s.to_string()) - .collect(), - fields: repo_info - .fields - .iter() - .map(|x| (x.to_string(), Field::from(Widget::default()))) - .collect(), - } - } - } - - 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 - } - } - - #[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: &Self::Key) -> Option; - async fn create( - &mut self, - context: &RepositoryContext, - data: Value, - ) -> Option; - async fn update( - &mut self, - context: &RepositoryContext, - id: &Self::Key, - data: Value, - ) -> Option; - async fn replace( - &mut self, - context: &RepositoryContext, - id: &Self::Key, - data: Value, - ) -> 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/mod.rs b/rear/src/admin/mod.rs deleted file mode 100644 index 4a5de72..0000000 --- a/rear/src/admin/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -use axum::{routing::get, Router}; - -pub mod domain; -pub mod state; -pub mod views; - -pub fn routes() -> Router { - Router::new() - .route("/", get(views::index::).post(views::index_action::)) - .route("/app/:app", get(views::list_app::)) - .route( - "/app/:app/model/:model", - get(views::list_item_collection::), - ) - .route( - "/app/:app/model/:model/add", - get(views::new_item::).post(views::create_item::), - ) - .route( - "/app/:app/model/:model/change/:id", - get(views::change_item::).patch(views::update_item::), - ) - .route( - "/app/:app/model/:model/detail/:id", - get(views::view_item_details::), - ) -} diff --git a/rear/src/admin/state.rs b/rear/src/admin/state.rs deleted file mode 100644 index 6adf430..0000000 --- a/rear/src/admin/state.rs +++ /dev/null @@ -1,174 +0,0 @@ -use tokio::sync::Mutex; - -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; - -pub trait AdminState { - fn get_templates(&self) -> &Templates; - fn get_registry(&self) -> SharedAdminRegistry; -} - -pub type SharedAdminRegistry = Arc; - -// main registry. -pub struct AdminRegistry { - base_path: String, - apps: HashMap, - models: HashMap, - repositories: HashMap>>, -} - -impl AdminRegistry { - pub fn new(base_path: &str) -> Self { - AdminRegistry { - base_path: base_path.to_owned(), - apps: HashMap::new(), - models: HashMap::new(), - repositories: HashMap::new(), - } - } - - pub fn get_apps(&self) -> Vec { - self.apps - .iter() - .map(|(key, node)| self.get_app(key, node)) - .collect() - } - - fn get_app(&self, key: &str, node: &internal::AdminApp) -> AdminApp { - let my_models = self.get_models(key); - AdminApp { - name: key.to_owned(), - key: node.name.to_owned(), - app_url: format!("/{}/app/{}", self.base_path, key.to_owned()), - models: my_models, - } - } - - pub fn register_app(&mut self, name: &str) -> String { - let key = self.get_key(name); - self.apps.insert( - key.to_owned(), - internal::AdminApp { - key: key.to_owned(), - name: name.to_owned(), - }, - ); - key - } - - fn get_key(&self, name: &str) -> String { - slug::slugify(name) - } - - fn model_from_internal(&self, internal_model: &internal::AdminModel) -> AdminModel { - let admin_url = format!( - "/{}/app/{}/model/{}", - self.base_path, internal_model.app_key, internal_model.model_key - ); - AdminModel { - key: internal_model.model_key.clone(), - name: internal_model.name.clone(), - view_only: false, - add_url: Some(format!("{}/add", admin_url)), - admin_url: admin_url, - } - } - - pub fn get_models(&self, app_key: &str) -> Vec { - self.models - .iter() - .filter(|(key, _)| key.starts_with(&format!("{}.", app_key))) - .map(|(_, model)| self.model_from_internal(model)) - .collect() - } - - pub fn get_model(&self, app_key: &str, model_key: &str) -> Option { - let full_model_key = format!("{}.{}", app_key, model_key); - let internal_model = self.models.get(&full_model_key)?; - - // unfinished: we need to think about model_key vs. model_id vs. entry_id, as "name" is ambiguous. - Some(self.model_from_internal(internal_model)) - } - - fn register_model_config(&mut self, model: AdminModelConfig) -> Result { - let local_config = internal::AdminModel::from(model); - if local_config.model_key.is_empty() { - return Err("No model name".to_owned()); - } - let local_config_name = format!("{}.{}", local_config.app_key, local_config.model_key); - if self.models.contains_key(&local_config_name) { - return Err(format!("Model {} already exists", local_config_name)); - } - let full_model_key = local_config_name.clone(); - self.models.insert(local_config_name, local_config); - Ok(full_model_key) - } - - pub fn register_model( - &mut self, - model: AdminModelConfig, - 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(()) - } - - pub fn get_repository( - &self, - app_key: &str, - model_key: &str, - ) -> 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 - return Ok(Arc::clone(repo)); - } else { - return Err("Couldn't find repository".to_owned()); - } - } -} - -mod internal { - // how the registry saves data internally. - - use super::super::domain::AdminModelConfig; - #[derive(Clone)] - pub struct AdminApp { - pub key: String, - pub name: String, - } - - #[derive(Clone)] - pub struct AdminModel { - pub app_key: String, - pub model_key: String, - pub name: String, - } - - impl From for AdminModel { - fn from(value: AdminModelConfig) -> Self { - AdminModel { - app_key: value.app_key, - model_key: slug::slugify(value.name.clone()), - name: value.name, - } - } - } - - impl From<(&str, &str)> for AdminModel { - fn from(value: (&str, &str)) -> Self { - AdminModel { - app_key: value.0.to_owned(), - model_key: slug::slugify(value.1.to_owned()), - name: value.1.to_owned(), - } - } - } -} diff --git a/rear/src/admin/views.rs b/rear/src/admin/views.rs deleted file mode 100644 index 040343a..0000000 --- a/rear/src/admin/views.rs +++ /dev/null @@ -1,350 +0,0 @@ -use axum::extract::Path; -use axum::http::HeaderMap; -use axum::Form; -use axum::{extract::State, response::IntoResponse}; -use log::info; -use serde_json::Value; - -use crate::admin::domain::{AdminApp, AdminModel}; -use crate::admin::state::AdminState; -use serde::{Deserialize, Serialize}; - -use super::domain::{RepositoryInfo, RepositoryItem, RepositoryList}; - -#[derive(Serialize, Deserialize)] -pub struct AdminRequest { - pub path: String, -} - -#[derive(Serialize)] -pub struct AdminContext { - pub base: Option, - pub language_code: Option, - pub language_bidi: Option, - pub user: Option, // Todo: user type - pub admin_url: String, - pub site_url: Option, - pub docsroot: Option, - pub messages: Vec, // Todo: message type - pub title: Option, - pub subtitle: Option, - pub content: String, - - pub request: AdminRequest, - pub available_apps: Vec, - pub item_model: Option, - pub item_info: Option, - pub item_list: RepositoryList, - pub item: Option, -} - -impl Default for AdminContext { - fn default() -> Self { - AdminContext { - base: None, // TODO: what is this used for? - language_code: Some("en-us".to_string()), // Default language code - language_bidi: Some(false), // Default language bidi - user: None, //UserType::default(), // Assuming UserType has a Default impl - admin_url: "/admin".to_owned(), - site_url: None, - docsroot: None, - messages: Vec::new(), // Empty vector for messages - title: None, - subtitle: None, - content: String::new(), // Empty string for content - available_apps: Vec::new(), - request: AdminRequest { - path: "".to_owned(), - }, - item_model: None, - item_info: None, - item_list: RepositoryList::Empty, - item: None, - } - } -} - -pub fn base_template(headers: &HeaderMap) -> Option { - let hx_request = headers.get("HX-Request").is_some(); - if hx_request { - Some("admin/base_hx.jinja".to_string()) - } else { - None - } -} - -pub async fn index( - admin: State, - headers: HeaderMap, -) -> impl IntoResponse { - let templates = admin.get_templates(); - let registry = admin.get_registry(); - templates.render_html( - "admin/index.html", - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - }, - ) -} - -// Index Action is POST to the index site. We can anchor some general business code here. -pub async fn index_action( - admin: State, -) -> impl IntoResponse { - "There is your answer!".to_owned() -} - -pub async fn list_app( - admin: State, - Path(app_key): Path, -) -> impl IntoResponse { - let templates = admin.get_templates(); - templates.render_html("admin/app_list.jinja", ()) -} - -// List Items renders the entire list item page. -pub async fn list_item_collection( - admin: State, - headers: HeaderMap, - Path((app_key, model_key)): Path<(String, String)>, -) -> impl IntoResponse { - info!("list_item_collection {} for model {}", app_key, model_key); - let templates = admin.get_templates(); - let registry = admin.get_registry(); - 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?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects. - 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_model: Some(admin_model), - ..Default::default() - } - } else { - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - } - }; - templates.render_html("admin/items/item_list.jinja", context) -} - -// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input. -pub async fn item_collection_action( - admin: State, - Path((app_key, model_key)): Path<(String, String)>, -) -> impl IntoResponse { - "There is your answer!".to_owned() -} - -// Item Details shows one single dataset. -pub async fn view_item_details( - admin: State, - headers: HeaderMap, - Path((app_key, model_key, id)): Path<(String, String, String)>, -) -> impl IntoResponse { - let templates = admin.get_templates(); - let registry = admin.get_registry(); - 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?"); - 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 { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - } - }; - templates.render_html("admin/items/item_detail.jinja", context) -} - -pub async fn new_item( - admin: State, - headers: HeaderMap, - Path((app_key, model_key)): Path<(String, String)>, -) -> impl IntoResponse { - let templates = admin.get_templates(); - let registry = admin.get_registry(); - 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?"); - 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_model: Some(admin_model), - ..Default::default() - } - } else { - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - } - }; - templates.render_html("admin/items/item_create.jinja", context) -} - -pub async fn create_item( - admin: State, - headers: HeaderMap, - Path((app_key, model_key)): Path<(String, String)>, - Form(form): Form, -) -> impl IntoResponse { - let templates = admin.get_templates(); - let registry = admin.get_registry(); - 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?"); - - // create our item. - let result = repo.create(&admin_model, form).await; - - // TODO: refactor run over these views, way too much repetition. - 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_model: Some(admin_model), - item: result, - ..Default::default() - } - } else { - AdminContext { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - } - }; - templates.render_html("admin/items/item_create.jinja", context) -} - -/// Change is the GET version. -pub async fn change_item( - admin: State, - headers: HeaderMap, - Path((app_key, model_key, id)): Path<(String, String, String)>, -) -> impl IntoResponse { - let templates = admin.get_templates(); - let registry = admin.get_registry(); - 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?"); - 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 { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - } - }; - templates.render_html("admin/items/item_change.jinja", context) -} - -pub async fn update_item( - admin: State, - headers: HeaderMap, - Path((app_key, model_key, id)): Path<(String, String, String)>, - Form(form): Form, -) -> impl IntoResponse { - let templates = admin.get_templates(); - let registry = admin.get_registry(); - 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?"); - 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 { - base: base_template(&headers), - available_apps: registry.get_apps(), - ..Default::default() - } - }; - let response = templates.render_html("admin/items/item_change.jinja", context); - response -} - -// Item Action allows running an action on one single dataset. -pub async fn item_action( - admin: State, - Path((app_key, model_key, model_id)): Path<(String, String, String)>, -) -> impl IntoResponse { - "There is your answer!".to_owned() -} - -pub async fn debug_view( - admin: State, - Path(data): Path, -) -> impl IntoResponse { - println!("debug: {}", data); - "Debug!".to_owned() -} diff --git a/rear/src/auth/models.rs b/rear/src/auth/models.rs deleted file mode 100644 index 058f20a..0000000 --- a/rear/src/auth/models.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub struct User { - pub id: i32, - pub username: String, -} diff --git a/rear/src/depot/mod.rs b/rear/src/depot/mod.rs index 96a1e59..9bfc512 100644 --- a/rear/src/depot/mod.rs +++ b/rear/src/depot/mod.rs @@ -11,21 +11,21 @@ pub mod widgets; pub fn routes() -> Router { Router::new() .route("/", get(views::index::).post(views::index_action::)) - .route("/app/:app", get(views::list_app::)) + .route("/:section", get(views::list_section::)) .route( - "/app/:app/model/:model", + "/:section/model/:model", get(views::list_item_collection::), ) .route( - "/app/:app/model/:model/add", + "/:section/model/:model/add", get(views::new_item::).post(views::create_item::), ) .route( - "/app/:app/model/:model/change/:id", + "/:section/model/:model/change/:id", get(views::change_item::).patch(views::update_item::), ) .route( - "/app/:app/model/:model/detail/:id", + "/:section/model/:model/detail/:id", get(views::view_item_details::), ) } diff --git a/rear/src/depot/prelude.rs b/rear/src/depot/prelude.rs index badea68..32625ec 100644 --- a/rear/src/depot/prelude.rs +++ b/rear/src/depot/prelude.rs @@ -1,7 +1,7 @@ pub use super::context::{DepotContext, DepotModel}; pub use super::registry::DepotRegistry; pub use super::repository::{ - DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem, - RepositoryList, + DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryError, + RepositoryInfo, RepositoryItem, RepositoryList, RepositoryResponse, RepositoryResult, }; pub use super::widgets::Widget; diff --git a/rear/src/depot/registry.rs b/rear/src/depot/registry.rs index 2ddddc0..5fd55a2 100644 --- a/rear/src/depot/registry.rs +++ b/rear/src/depot/registry.rs @@ -35,7 +35,7 @@ impl DepotRegistry { DepotSection { key: key.to_owned(), name: section_info.name.to_owned(), - section_url: format!("/{}/section/{}", self.base_path, key.to_owned()), + section_url: format!("/{}/{}", self.base_path, key.to_owned()), models: my_models, } } @@ -58,7 +58,7 @@ impl DepotRegistry { fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel { let model_url = format!( - "/{}/section/{}/model/{}", + "/{}/{}/model/{}", self.base_path, model_info.section_key, model_info.model_key ); DepotModel { @@ -110,7 +110,7 @@ impl DepotRegistry { Ok(()) } - pub fn get_repository( + pub(crate) fn get_repository( &self, section_key: &str, model_key: &str, diff --git a/rear/src/depot/repository.rs b/rear/src/depot/repository.rs index 1bb5753..ed7657d 100644 --- a/rear/src/depot/repository.rs +++ b/rear/src/depot/repository.rs @@ -4,6 +4,7 @@ use serde_json::Value; use std::any::Any; use std::fmt::Debug; use std::vec::IntoIter; +use thiserror::Error; use super::context::DepotModel; use super::widgets::Widget; @@ -80,6 +81,32 @@ pub struct RepositoryItem { pub change_url: Option, } +pub enum RepositoryResponse { + NoItem, + ItemOnly(RepositoryItem), + ItemAndHeaders(RepositoryItem, axum::http::HeaderMap), +} + +impl Into> for RepositoryResponse { + fn into(self) -> Option { + match self { + RepositoryResponse::NoItem => None, + RepositoryResponse::ItemOnly(some) => Some(some), + RepositoryResponse::ItemAndHeaders(some, _) => Some(some), + } + } +} + +#[derive(Error, Debug)] +pub enum RepositoryError { + #[error("an unknown occurred: {0}")] + UnknownError(String), + #[error("key not found in downcast?")] + KeyNotFound, +} + +pub type RepositoryResult = Result; + pub enum RepositoryList { Empty, List { @@ -226,20 +253,20 @@ pub trait DepotRepository: Send + Sync { async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; async fn list(&self, context: &RepositoryContext) -> RepositoryList; - async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option; - async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option; + async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> RepositoryResult; + async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult; async fn update( &mut self, context: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option; + ) -> RepositoryResult; async fn replace( &mut self, context: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option; + ) -> RepositoryResult; async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option; } @@ -248,24 +275,20 @@ pub(crate) trait DynDepotRepository: 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 get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult; + async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult; async fn update( &mut self, context: &RepositoryContext, id: &dyn PrimaryKeyType, data: Value, - ) -> Option; + ) -> RepositoryResult; async fn replace( &mut self, context: &RepositoryContext, id: &dyn PrimaryKeyType, data: Value, - ) -> Option; + ) -> RepositoryResult; async fn delete( &mut self, context: &RepositoryContext, @@ -305,19 +328,15 @@ impl DynDepotRepository for DepotRepositoryWrapper { self.inner.list(context).await } - async fn get( - &self, - context: &RepositoryContext, - id: &dyn PrimaryKeyType, - ) -> Option { + async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult { if let Some(key) = id.as_any().downcast_ref::() { self.inner.get(context, key).await } else { - None + Err(RepositoryError::KeyNotFound) } } - async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option { + async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult { self.inner.create(context, data).await } @@ -326,11 +345,11 @@ impl DynDepotRepository for DepotRepositoryWrapper { context: &RepositoryContext, id: &dyn PrimaryKeyType, data: Value, - ) -> Option { + ) -> RepositoryResult { if let Some(key) = id.as_any().downcast_ref::() { self.inner.update(context, key, data).await } else { - None + Err(RepositoryError::KeyNotFound) } } @@ -339,11 +358,11 @@ impl DynDepotRepository for DepotRepositoryWrapper { context: &RepositoryContext, id: &dyn PrimaryKeyType, data: Value, - ) -> Option { + ) -> RepositoryResult { if let Some(key) = id.as_any().downcast_ref::() { self.inner.replace(context, key, data).await } else { - None + Err(RepositoryError::KeyNotFound) } } diff --git a/rear/src/depot/views.rs b/rear/src/depot/views.rs index 6bd57fd..f77996e 100644 --- a/rear/src/depot/views.rs +++ b/rear/src/depot/views.rs @@ -40,7 +40,7 @@ pub async fn index_action( "There is your answer!".to_owned() } -pub async fn list_app( +pub async fn list_section( depot: State, Path(depot_key): Path, ) -> impl IntoResponse { @@ -107,7 +107,7 @@ pub async fn view_item_details( sections: registry.get_sections(), item_info: Some(repo.info(&depot_model).await), item_list: repo.list(&depot_model).await, - item: repo.get(&depot_model, key.as_ref()).await, + item: repo.get(&depot_model, key.as_ref()).await.unwrap().into(), item_model: Some(depot_model), ..Default::default() } @@ -182,7 +182,7 @@ pub async fn create_item( item_info: Some(repo.info(&depot_model).await), item_list: repo.list(&depot_model).await, item_model: Some(depot_model), - item: result, + item: result.unwrap().into(), ..Default::default() } } else { @@ -207,18 +207,19 @@ pub async fn change_item( let repo = repo.lock().await; let depot_model = registry .get_model(&depot_key, &model_key) - .expect("depot Model not found?"); + .expect("Depot Model not found?"); if let Some(key) = repo.key_from_string(id) { DepotContext { base: base_template(&headers), sections: registry.get_sections(), item_info: Some(repo.info(&depot_model).await), item_list: repo.list(&depot_model).await, - item: repo.get(&depot_model, key.as_ref()).await, + item: repo.get(&depot_model, key.as_ref()).await.unwrap().into(), item_model: Some(depot_model), ..Default::default() } } else { + // Repository did not have key. DepotContext { base: base_template(&headers), sections: registry.get_sections(), @@ -226,6 +227,7 @@ pub async fn change_item( } } } else { + // Repository could not be loaded. DepotContext { base: base_template(&headers), sections: registry.get_sections(), @@ -255,7 +257,7 @@ pub async fn update_item( sections: registry.get_sections(), item_info: Some(repo.info(&depot_model).await), item_list: repo.list(&depot_model).await, - item: result, + item: result.unwrap().into(), item_model: Some(depot_model), ..Default::default() } diff --git a/rear_auth/src/user_admin_repository.rs b/rear_auth/src/user_admin_repository.rs index 0ca05c4..eb5d514 100644 --- a/rear_auth/src/user_admin_repository.rs +++ b/rear_auth/src/user_admin_repository.rs @@ -45,25 +45,25 @@ impl DepotRepository for UserRepository { .set_widget("is_superuser", Widget::checkbox()) } - async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult { 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, + if let Ok(get_user) = get_user { + if let Some(user) = get_user { + let id = user.id.to_string(); + match serde_json::to_value(&user) { + Ok(item) => { + return Ok(RepositoryResponse::ItemOnly(model.build_item(&*id, item))); + } + Err(_) => { + return Err(RepositoryError::UnknownError( + "JSON Error creating value".to_owned(), + )) } } } - Err(_) => return None, } - - None + Ok(RepositoryResponse::NoItem) } async fn list(&self, model: &RepositoryContext) -> RepositoryList { @@ -87,7 +87,7 @@ impl DepotRepository for UserRepository { } } - async fn create(&mut self, model: &RepositoryContext, data: Value) -> Option { + async fn create(&mut self, model: &RepositoryContext, data: Value) -> RepositoryResult { if let Value::Object(data) = data { let username = data.get("username").unwrap().as_str().unwrap(); let password = data.get("password").unwrap().as_str().unwrap(); @@ -119,10 +119,11 @@ impl DepotRepository for UserRepository { if let Ok(user) = user.insert(&self.connection).await { let id = user.id.to_string(); - return Some(model.build_item(&*id, serde_json::to_value(&user).unwrap())); + let item = model.build_item(&*id, serde_json::to_value(&user).unwrap()); + return Ok(RepositoryResponse::ItemOnly(item)); } } - None + Ok(RepositoryResponse::NoItem) } async fn update( @@ -130,7 +131,7 @@ impl DepotRepository for UserRepository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { + ) -> RepositoryResult { let id: i32 = *id as i32; let user: Option = entity::User::find_by_id(id) .one(&self.connection) @@ -171,10 +172,12 @@ impl DepotRepository for UserRepository { // 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())); + return Ok(RepositoryResponse::ItemOnly( + model.build_item(&*id, serde_json::to_value(&user).unwrap()), + )); } - None + Ok(RepositoryResponse::NoItem) } async fn replace( @@ -182,7 +185,7 @@ impl DepotRepository for UserRepository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { + ) -> RepositoryResult { self.update(model, id, data).await } diff --git a/src/admin_examples/empty_repository.rs b/src/admin_examples/empty_repository.rs index f0327c6..057fa7b 100644 --- a/src/admin_examples/empty_repository.rs +++ b/src/admin_examples/empty_repository.rs @@ -34,17 +34,13 @@ impl DepotRepository for Repository { } // POST on item collection. - async fn create( - &mut self, - model: &RepositoryContext, - mut data: Value, - ) -> Option { - None + async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // GET single item. - async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { - None + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // PATCH single item. @@ -53,8 +49,8 @@ impl DepotRepository for Repository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { - None + ) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // PUT single item. @@ -63,8 +59,8 @@ impl DepotRepository for Repository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { - None + ) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // DELETE single item. diff --git a/src/admin_examples/file_repository.rs b/src/admin_examples/file_repository.rs index a50bcc3..783cb50 100644 --- a/src/admin_examples/file_repository.rs +++ b/src/admin_examples/file_repository.rs @@ -61,17 +61,13 @@ impl DepotRepository for FileRepository { } // POST on item collection. - async fn create( - &mut self, - model: &RepositoryContext, - mut data: Value, - ) -> Option { - None + async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // GET single item. - async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { - None + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // PATCH single item. @@ -80,8 +76,8 @@ impl DepotRepository for FileRepository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { - None + ) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // PUT single item. @@ -90,8 +86,8 @@ impl DepotRepository for FileRepository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { - None + ) -> RepositoryResult { + Ok(RepositoryResponse::NoItem) } // DELETE single item. diff --git a/src/admin_examples/static_repository.rs b/src/admin_examples/static_repository.rs index 02f8554..8526b83 100644 --- a/src/admin_examples/static_repository.rs +++ b/src/admin_examples/static_repository.rs @@ -57,11 +57,13 @@ impl DepotRepository for MyStaticRepository { //.set_widget("name", Widget::textarea().options(&[("disabled", "true")])) } - async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option { + async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult { 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)) + Ok(RepositoryResponse::ItemOnly( + model.build_item(&*id.to_string(), item), + )) } async fn list(&self, model: &RepositoryContext) -> RepositoryList { @@ -75,11 +77,7 @@ impl DepotRepository for MyStaticRepository { } } - async fn create( - &mut self, - model: &RepositoryContext, - mut data: Value, - ) -> Option { + async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult { debug!("Asked to create: {}", data); let new_id = self.next_id; @@ -93,7 +91,9 @@ impl DepotRepository for MyStaticRepository { self.content.push(data.clone()); // Return the newly created item - Some(model.build_item(&*new_id.to_string(), data)) + Ok(RepositoryResponse::ItemOnly( + model.build_item(&*new_id.to_string(), data), + )) } async fn update( @@ -101,7 +101,7 @@ impl DepotRepository for MyStaticRepository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { + ) -> RepositoryResult { debug!("I would now update: {}, {}", id, data); // First, find the index of the item to update @@ -119,11 +119,13 @@ impl DepotRepository for MyStaticRepository { *item = data.clone(); if let Some(item_id) = item.get("id") { - return Some(model.build_item(&*item_id.to_string(), data)); + return Ok(RepositoryResponse::ItemOnly( + model.build_item(&*item_id.to_string(), data), + )); } } - None + Ok(RepositoryResponse::NoItem) } async fn replace( @@ -131,7 +133,7 @@ impl DepotRepository for MyStaticRepository { model: &RepositoryContext, id: &Self::Key, data: Value, - ) -> Option { + ) -> RepositoryResult { self.update(model, id, data).await }