refactor: result type, removing rear.admin

This commit is contained in:
Gabor Körber 2024-07-23 00:18:04 +02:00
parent 966291dbd9
commit 843e432ec4
16 changed files with 127 additions and 1140 deletions

17
Cargo.lock generated
View File

@ -2005,6 +2005,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -2464,6 +2470,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"slug", "slug",
"thiserror",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -3459,12 +3466,13 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.31" version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
"num-conv",
"powerfmt", "powerfmt",
"serde", "serde",
"time-core", "time-core",
@ -3479,10 +3487,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.16" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [ dependencies = [
"num-conv",
"time-core", "time-core",
] ]

View File

@ -26,3 +26,4 @@ serde_json = "1.0.108"
slug = "0.1.5" slug = "0.1.5"
async-trait = "0.1.77" async-trait = "0.1.77"
tracing = "0.1.40" tracing = "0.1.40"
thiserror = "1.0.61"

View File

@ -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<String>,
}
#[derive(Deserialize, Serialize)]
pub struct AdminApp {
pub key: String,
pub name: String,
pub app_url: String,
pub models: Vec<AdminModel>,
}
}
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<String> {
Some(format!("{}/detail/{}", self.admin_url, key))
}
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
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<String>,
#[serde(rename = "type")]
field_type: String,
readonly: bool,
required: bool,
options: Value,
}
impl From<Widget> 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::<serde_json::Map<String, Value>>()
.into(),
}
}
}
#[derive(Serialize)]
pub struct RepositoryItem {
pub fields: Value,
pub detail_url: Option<String>,
pub change_url: Option<String>,
}
pub enum RepositoryList {
Empty,
List {
values: Vec<RepositoryItem>,
},
Page {
values: Vec<RepositoryItem>,
offset: usize,
total: usize,
},
Stream {
values: Vec<RepositoryItem>,
next_index: Option<String>,
},
}
impl IntoIterator for RepositoryList {
type Item = RepositoryItem;
type IntoIter = IntoIter<Self::Item>;
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<String>,
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<T: Into<Field>>(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<RepoInfo> 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<Self::Key>;
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem>;
async fn create(
&mut self,
context: &RepositoryContext,
data: Value,
) -> Option<RepositoryItem>;
async fn update(
&mut self,
context: &RepositoryContext,
id: &Self::Key,
data: Value,
) -> Option<RepositoryItem>;
async fn replace(
&mut self,
context: &RepositoryContext,
id: &Self::Key,
data: Value,
) -> Option<RepositoryItem>;
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
}
#[async_trait]
pub trait DynAdminRepository: Send + Sync {
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
async fn get(
&self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<RepositoryItem>;
async fn create(
&mut self,
context: &RepositoryContext,
data: Value,
) -> Option<RepositoryItem>;
async fn update(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem>;
async fn replace(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem>;
async fn delete(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<Value>;
}
pub struct AdminRepositoryWrapper<T: AdminRepository> {
inner: T,
}
impl<T: AdminRepository> AdminRepositoryWrapper<T> {
pub fn new(inner: T) -> Self {
Self { inner }
}
fn key_from_string(&self, s: String) -> Option<<T as AdminRepository>::Key> {
self.inner.key_from_string(s)
}
}
#[async_trait]
impl<T: AdminRepository> DynAdminRepository for AdminRepositoryWrapper<T> {
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
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<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.get(context, key).await
} else {
None
}
}
async fn create(
&mut self,
context: &RepositoryContext,
data: Value,
) -> Option<RepositoryItem> {
self.inner.create(context, data).await
}
async fn update(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.update(context, key, data).await
} else {
None
}
}
async fn replace(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.replace(context, key, data).await
} else {
None
}
}
async fn delete(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<Value> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.delete(context, key).await
} else {
None
}
}
}
}

View File

@ -1,27 +0,0 @@
use axum::{routing::get, Router};
pub mod domain;
pub mod state;
pub mod views;
pub fn routes<S: state::AdminState + Clone + Send + Sync + 'static>() -> Router<S> {
Router::new()
.route("/", get(views::index::<S>).post(views::index_action::<S>))
.route("/app/:app", get(views::list_app::<S>))
.route(
"/app/:app/model/:model",
get(views::list_item_collection::<S>),
)
.route(
"/app/:app/model/:model/add",
get(views::new_item::<S>).post(views::create_item::<S>),
)
.route(
"/app/:app/model/:model/change/:id",
get(views::change_item::<S>).patch(views::update_item::<S>),
)
.route(
"/app/:app/model/:model/detail/:id",
get(views::view_item_details::<S>),
)
}

View File

@ -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<AdminRegistry>;
// main registry.
pub struct AdminRegistry {
base_path: String,
apps: HashMap<String, internal::AdminApp>,
models: HashMap<String, internal::AdminModel>,
repositories: HashMap<String, Arc<Mutex<dyn DynAdminRepository>>>,
}
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<AdminApp> {
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<AdminModel> {
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<AdminModel> {
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<String, String> {
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<R: AdminRepository + 'static>(
&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<Arc<Mutex<dyn DynAdminRepository>>, 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<AdminModelConfig> 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(),
}
}
}
}

View File

@ -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<String>,
pub language_code: Option<String>,
pub language_bidi: Option<bool>,
pub user: Option<String>, // Todo: user type
pub admin_url: String,
pub site_url: Option<String>,
pub docsroot: Option<String>,
pub messages: Vec<String>, // Todo: message type
pub title: Option<String>,
pub subtitle: Option<String>,
pub content: String,
pub request: AdminRequest,
pub available_apps: Vec<AdminApp>,
pub item_model: Option<AdminModel>,
pub item_info: Option<RepositoryInfo>,
pub item_list: RepositoryList,
pub item: Option<RepositoryItem>,
}
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<String> {
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
) -> impl IntoResponse {
"There is your answer!".to_owned()
}
pub async fn list_app<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
Path(app_key): Path<String>,
) -> 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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>,
Form(form): Form<Value>,
) -> 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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
headers: HeaderMap,
Path((app_key, model_key, id)): Path<(String, String, String)>,
Form(form): Form<Value>,
) -> 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<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
Path((app_key, model_key, model_id)): Path<(String, String, String)>,
) -> impl IntoResponse {
"There is your answer!".to_owned()
}
pub async fn debug_view<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
Path(data): Path<String>,
) -> impl IntoResponse {
println!("debug: {}", data);
"Debug!".to_owned()
}

View File

@ -1,4 +0,0 @@
pub struct User {
pub id: i32,
pub username: String,
}

View File

@ -11,21 +11,21 @@ pub mod widgets;
pub fn routes<S: state::DepotState + Clone + Send + Sync + 'static>() -> Router<S> { pub fn routes<S: state::DepotState + Clone + Send + Sync + 'static>() -> Router<S> {
Router::new() Router::new()
.route("/", get(views::index::<S>).post(views::index_action::<S>)) .route("/", get(views::index::<S>).post(views::index_action::<S>))
.route("/app/:app", get(views::list_app::<S>)) .route("/:section", get(views::list_section::<S>))
.route( .route(
"/app/:app/model/:model", "/:section/model/:model",
get(views::list_item_collection::<S>), get(views::list_item_collection::<S>),
) )
.route( .route(
"/app/:app/model/:model/add", "/:section/model/:model/add",
get(views::new_item::<S>).post(views::create_item::<S>), get(views::new_item::<S>).post(views::create_item::<S>),
) )
.route( .route(
"/app/:app/model/:model/change/:id", "/:section/model/:model/change/:id",
get(views::change_item::<S>).patch(views::update_item::<S>), get(views::change_item::<S>).patch(views::update_item::<S>),
) )
.route( .route(
"/app/:app/model/:model/detail/:id", "/:section/model/:model/detail/:id",
get(views::view_item_details::<S>), get(views::view_item_details::<S>),
) )
} }

View File

@ -1,7 +1,7 @@
pub use super::context::{DepotContext, DepotModel}; pub use super::context::{DepotContext, DepotModel};
pub use super::registry::DepotRegistry; pub use super::registry::DepotRegistry;
pub use super::repository::{ pub use super::repository::{
DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem, DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryError,
RepositoryList, RepositoryInfo, RepositoryItem, RepositoryList, RepositoryResponse, RepositoryResult,
}; };
pub use super::widgets::Widget; pub use super::widgets::Widget;

View File

@ -35,7 +35,7 @@ impl DepotRegistry {
DepotSection { DepotSection {
key: key.to_owned(), key: key.to_owned(),
name: section_info.name.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, models: my_models,
} }
} }
@ -58,7 +58,7 @@ impl DepotRegistry {
fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel { fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel {
let model_url = format!( let model_url = format!(
"/{}/section/{}/model/{}", "/{}/{}/model/{}",
self.base_path, model_info.section_key, model_info.model_key self.base_path, model_info.section_key, model_info.model_key
); );
DepotModel { DepotModel {
@ -110,7 +110,7 @@ impl DepotRegistry {
Ok(()) Ok(())
} }
pub fn get_repository( pub(crate) fn get_repository(
&self, &self,
section_key: &str, section_key: &str,
model_key: &str, model_key: &str,

View File

@ -4,6 +4,7 @@ use serde_json::Value;
use std::any::Any; use std::any::Any;
use std::fmt::Debug; use std::fmt::Debug;
use std::vec::IntoIter; use std::vec::IntoIter;
use thiserror::Error;
use super::context::DepotModel; use super::context::DepotModel;
use super::widgets::Widget; use super::widgets::Widget;
@ -80,6 +81,32 @@ pub struct RepositoryItem {
pub change_url: Option<String>, pub change_url: Option<String>,
} }
pub enum RepositoryResponse {
NoItem,
ItemOnly(RepositoryItem),
ItemAndHeaders(RepositoryItem, axum::http::HeaderMap),
}
impl Into<Option<RepositoryItem>> for RepositoryResponse {
fn into(self) -> Option<RepositoryItem> {
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<RepositoryResponse, RepositoryError>;
pub enum RepositoryList { pub enum RepositoryList {
Empty, Empty,
List { List {
@ -226,20 +253,20 @@ pub trait DepotRepository: Send + Sync {
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
async fn list(&self, context: &RepositoryContext) -> RepositoryList; async fn list(&self, context: &RepositoryContext) -> RepositoryList;
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem>; async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> RepositoryResult;
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem>; async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult;
async fn update( async fn update(
&mut self, &mut self,
context: &RepositoryContext, context: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem>; ) -> RepositoryResult;
async fn replace( async fn replace(
&mut self, &mut self,
context: &RepositoryContext, context: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem>; ) -> RepositoryResult;
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>; async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
} }
@ -248,24 +275,20 @@ pub(crate) trait DynDepotRepository: Send + Sync {
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>; fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo; async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
async fn list(&self, context: &RepositoryContext) -> RepositoryList; async fn list(&self, context: &RepositoryContext) -> RepositoryList;
async fn get( async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult;
&self, async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult;
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<RepositoryItem>;
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem>;
async fn update( async fn update(
&mut self, &mut self,
context: &RepositoryContext, context: &RepositoryContext,
id: &dyn PrimaryKeyType, id: &dyn PrimaryKeyType,
data: Value, data: Value,
) -> Option<RepositoryItem>; ) -> RepositoryResult;
async fn replace( async fn replace(
&mut self, &mut self,
context: &RepositoryContext, context: &RepositoryContext,
id: &dyn PrimaryKeyType, id: &dyn PrimaryKeyType,
data: Value, data: Value,
) -> Option<RepositoryItem>; ) -> RepositoryResult;
async fn delete( async fn delete(
&mut self, &mut self,
context: &RepositoryContext, context: &RepositoryContext,
@ -305,19 +328,15 @@ impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
self.inner.list(context).await self.inner.list(context).await
} }
async fn get( async fn get(&self, context: &RepositoryContext, id: &dyn PrimaryKeyType) -> RepositoryResult {
&self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() { if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.get(context, key).await self.inner.get(context, key).await
} else { } else {
None Err(RepositoryError::KeyNotFound)
} }
} }
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem> { async fn create(&mut self, context: &RepositoryContext, data: Value) -> RepositoryResult {
self.inner.create(context, data).await self.inner.create(context, data).await
} }
@ -326,11 +345,11 @@ impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
context: &RepositoryContext, context: &RepositoryContext,
id: &dyn PrimaryKeyType, id: &dyn PrimaryKeyType,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() { if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.update(context, key, data).await self.inner.update(context, key, data).await
} else { } else {
None Err(RepositoryError::KeyNotFound)
} }
} }
@ -339,11 +358,11 @@ impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
context: &RepositoryContext, context: &RepositoryContext,
id: &dyn PrimaryKeyType, id: &dyn PrimaryKeyType,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() { if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.replace(context, key, data).await self.inner.replace(context, key, data).await
} else { } else {
None Err(RepositoryError::KeyNotFound)
} }
} }

View File

@ -40,7 +40,7 @@ pub async fn index_action<S: DepotState + Clone + Send + Sync + 'static>(
"There is your answer!".to_owned() "There is your answer!".to_owned()
} }
pub async fn list_app<S: DepotState + Clone + Send + Sync + 'static>( pub async fn list_section<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>, depot: State<S>,
Path(depot_key): Path<String>, Path(depot_key): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
@ -107,7 +107,7 @@ pub async fn view_item_details<S: DepotState + Clone + Send + Sync + 'static>(
sections: registry.get_sections(), sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await), item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&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), item_model: Some(depot_model),
..Default::default() ..Default::default()
} }
@ -182,7 +182,7 @@ pub async fn create_item<S: DepotState + Clone + Send + Sync + 'static>(
item_info: Some(repo.info(&depot_model).await), item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await, item_list: repo.list(&depot_model).await,
item_model: Some(depot_model), item_model: Some(depot_model),
item: result, item: result.unwrap().into(),
..Default::default() ..Default::default()
} }
} else { } else {
@ -207,18 +207,19 @@ pub async fn change_item<S: DepotState + Clone + Send + Sync + 'static>(
let repo = repo.lock().await; let repo = repo.lock().await;
let depot_model = registry let depot_model = registry
.get_model(&depot_key, &model_key) .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) { if let Some(key) = repo.key_from_string(id) {
DepotContext { DepotContext {
base: base_template(&headers), base: base_template(&headers),
sections: registry.get_sections(), sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await), item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&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), item_model: Some(depot_model),
..Default::default() ..Default::default()
} }
} else { } else {
// Repository did not have key.
DepotContext { DepotContext {
base: base_template(&headers), base: base_template(&headers),
sections: registry.get_sections(), sections: registry.get_sections(),
@ -226,6 +227,7 @@ pub async fn change_item<S: DepotState + Clone + Send + Sync + 'static>(
} }
} }
} else { } else {
// Repository could not be loaded.
DepotContext { DepotContext {
base: base_template(&headers), base: base_template(&headers),
sections: registry.get_sections(), sections: registry.get_sections(),
@ -255,7 +257,7 @@ pub async fn update_item<S: DepotState + Clone + Send + Sync + 'static>(
sections: registry.get_sections(), sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await), item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await, item_list: repo.list(&depot_model).await,
item: result, item: result.unwrap().into(),
item_model: Some(depot_model), item_model: Some(depot_model),
..Default::default() ..Default::default()
} }

View File

@ -45,25 +45,25 @@ impl DepotRepository for UserRepository {
.set_widget("is_superuser", Widget::checkbox()) .set_widget("is_superuser", Widget::checkbox())
} }
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> { async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
let id: i32 = *id as i32; // use try_into() instead. let id: i32 = *id as i32; // use try_into() instead.
let get_user = entity::User::find_by_id(id).one(&self.connection).await; let get_user = entity::User::find_by_id(id).one(&self.connection).await;
match get_user { if let Ok(get_user) = get_user {
Ok(get_user) => { if let Some(user) = get_user {
if let Some(user) = get_user { let id = user.id.to_string();
let id = user.id.to_string(); match serde_json::to_value(&user) {
match serde_json::to_value(&user) { Ok(item) => {
Ok(item) => { return Ok(RepositoryResponse::ItemOnly(model.build_item(&*id, item)));
return Some(model.build_item(&*id, item)); }
} Err(_) => {
Err(_) => return None, return Err(RepositoryError::UnknownError(
"JSON Error creating value".to_owned(),
))
} }
} }
} }
Err(_) => return None,
} }
Ok(RepositoryResponse::NoItem)
None
} }
async fn list(&self, model: &RepositoryContext) -> RepositoryList { 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<RepositoryItem> { async fn create(&mut self, model: &RepositoryContext, data: Value) -> RepositoryResult {
if let Value::Object(data) = data { if let Value::Object(data) = data {
let username = data.get("username").unwrap().as_str().unwrap(); let username = data.get("username").unwrap().as_str().unwrap();
let password = data.get("password").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 { if let Ok(user) = user.insert(&self.connection).await {
let id = user.id.to_string(); 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( async fn update(
@ -130,7 +131,7 @@ impl DepotRepository for UserRepository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
let id: i32 = *id as i32; let id: i32 = *id as i32;
let user: Option<entity::user::Model> = entity::User::find_by_id(id) let user: Option<entity::user::Model> = entity::User::find_by_id(id)
.one(&self.connection) .one(&self.connection)
@ -171,10 +172,12 @@ impl DepotRepository for UserRepository {
// update // update
if let Ok(user) = user.update(&self.connection).await { if let Ok(user) = user.update(&self.connection).await {
let id = user.id.to_string(); 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( async fn replace(
@ -182,7 +185,7 @@ impl DepotRepository for UserRepository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
self.update(model, id, data).await self.update(model, id, data).await
} }

View File

@ -34,17 +34,13 @@ impl DepotRepository for Repository {
} }
// POST on item collection. // POST on item collection.
async fn create( async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
&mut self, Ok(RepositoryResponse::NoItem)
model: &RepositoryContext,
mut data: Value,
) -> Option<RepositoryItem> {
None
} }
// GET single item. // GET single item.
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> { async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
None Ok(RepositoryResponse::NoItem)
} }
// PATCH single item. // PATCH single item.
@ -53,8 +49,8 @@ impl DepotRepository for Repository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
None Ok(RepositoryResponse::NoItem)
} }
// PUT single item. // PUT single item.
@ -63,8 +59,8 @@ impl DepotRepository for Repository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
None Ok(RepositoryResponse::NoItem)
} }
// DELETE single item. // DELETE single item.

View File

@ -61,17 +61,13 @@ impl DepotRepository for FileRepository {
} }
// POST on item collection. // POST on item collection.
async fn create( async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
&mut self, Ok(RepositoryResponse::NoItem)
model: &RepositoryContext,
mut data: Value,
) -> Option<RepositoryItem> {
None
} }
// GET single item. // GET single item.
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> { async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
None Ok(RepositoryResponse::NoItem)
} }
// PATCH single item. // PATCH single item.
@ -80,8 +76,8 @@ impl DepotRepository for FileRepository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
None Ok(RepositoryResponse::NoItem)
} }
// PUT single item. // PUT single item.
@ -90,8 +86,8 @@ impl DepotRepository for FileRepository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
None Ok(RepositoryResponse::NoItem)
} }
// DELETE single item. // DELETE single item.

View File

@ -57,11 +57,13 @@ impl DepotRepository for MyStaticRepository {
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")])) //.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
} }
async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem> { async fn get(&self, model: &RepositoryContext, id: &Self::Key) -> RepositoryResult {
let id = *id as usize; let id = *id as usize;
let item = self.content.get(id - 1).cloned().unwrap(); let item = self.content.get(id - 1).cloned().unwrap();
let id = item.get("id").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 { async fn list(&self, model: &RepositoryContext) -> RepositoryList {
@ -75,11 +77,7 @@ impl DepotRepository for MyStaticRepository {
} }
} }
async fn create( async fn create(&mut self, model: &RepositoryContext, mut data: Value) -> RepositoryResult {
&mut self,
model: &RepositoryContext,
mut data: Value,
) -> Option<RepositoryItem> {
debug!("Asked to create: {}", data); debug!("Asked to create: {}", data);
let new_id = self.next_id; let new_id = self.next_id;
@ -93,7 +91,9 @@ impl DepotRepository for MyStaticRepository {
self.content.push(data.clone()); self.content.push(data.clone());
// Return the newly created item // 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( async fn update(
@ -101,7 +101,7 @@ impl DepotRepository for MyStaticRepository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
debug!("I would now update: {}, {}", id, data); debug!("I would now update: {}, {}", id, data);
// First, find the index of the item to update // First, find the index of the item to update
@ -119,11 +119,13 @@ impl DepotRepository for MyStaticRepository {
*item = data.clone(); *item = data.clone();
if let Some(item_id) = item.get("id") { 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( async fn replace(
@ -131,7 +133,7 @@ impl DepotRepository for MyStaticRepository {
model: &RepositoryContext, model: &RepositoryContext,
id: &Self::Key, id: &Self::Key,
data: Value, data: Value,
) -> Option<RepositoryItem> { ) -> RepositoryResult {
self.update(model, id, data).await self.update(model, id, data).await
} }