code: functioning example for repository registry; using tokio::sync::Mutex for read/write lock; prototype create

This commit is contained in:
Gabor Körber 2024-01-28 18:11:02 +01:00
parent 9feb2f4ae7
commit 0e6649de1b
6 changed files with 126 additions and 32 deletions

View File

@ -17,13 +17,16 @@ edit functionality:
-> how would i create widgets? programmatically? in templates? -> how would i create widgets? programmatically? in templates?
-> maybe even both approaches available? -> maybe even both approaches available?
-> need a list of MVP widgets, and a way to extend them. -> need a list of MVP widgets, and a way to extend them.
-> widgets should be groupable.
-> taking a look at djangos solution!
-> are there any smart json data => form things out there?
- validation. - validation.
-> client side as well? -> client side as well? fomantic has it.
-> existing solutions? -> existing solutions?
-> transparent way to display error messages at the appropriate field (probably adding results into responses) -> transparent way to display error messages at the appropriate field (probably adding results into responses)
-> required -> handling: "required"
-> invalid -> handling: "invalid"
-> invalid(max-length) -> example implementation: invalid(max-length)
auth: auth:
- user model. - user model.

View File

@ -56,6 +56,7 @@ pub mod repository {
use serde_json::Value; use serde_json::Value;
use std::vec::IntoIter; use std::vec::IntoIter;
#[derive(PartialEq)]
pub enum LookupKey { pub enum LookupKey {
Integer(usize), Integer(usize),
String(String), String(String),
@ -152,6 +153,7 @@ pub mod repository {
pub name: &'static str, pub name: &'static str,
pub lookup_key: &'static str, pub lookup_key: &'static str,
pub display_list: &'static [&'static str], pub display_list: &'static [&'static str],
pub fields: &'static [&'static str],
} }
// consider builder: https://docs.rs/derive_builder/latest/derive_builder/ // consider builder: https://docs.rs/derive_builder/latest/derive_builder/
@ -163,6 +165,7 @@ pub mod repository {
name: String, name: String,
lookup_key: String, lookup_key: String,
display_list: Vec<String>, display_list: Vec<String>,
fields: Vec<String>,
} }
impl RepositoryInfo { impl RepositoryInfo {
@ -171,6 +174,7 @@ pub mod repository {
name: name.to_owned(), name: name.to_owned(),
lookup_key: lookup_key.to_owned(), lookup_key: lookup_key.to_owned(),
display_list: vec![], display_list: vec![],
fields: vec![],
} }
} }
@ -192,6 +196,7 @@ pub mod repository {
.iter() .iter()
.map(|&s| s.to_string()) .map(|&s| s.to_string())
.collect(), .collect(),
fields: repo_info.fields.iter().map(|&s| s.to_string()).collect(),
} }
} }
} }
@ -200,8 +205,13 @@ pub mod repository {
fn info(&self, model: &AdminModel) -> RepositoryInfo; fn info(&self, model: &AdminModel) -> RepositoryInfo;
fn list(&self, model: &AdminModel) -> RepositoryList; fn list(&self, model: &AdminModel) -> RepositoryList;
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>; fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
fn create(&self, model: &AdminModel, data: Value) -> Option<RepositoryItem>; fn create(&mut self, model: &AdminModel, data: Value) -> Option<RepositoryItem>;
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem>; fn update(
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value>; &mut self,
model: &AdminModel,
id: LookupKey,
data: Value,
) -> Option<RepositoryItem>;
fn delete(&mut self, model: &AdminModel, id: LookupKey) -> Option<Value>;
} }
} }

View File

@ -2,16 +2,20 @@
use super::domain::*; use super::domain::*;
use super::state::AdminRegistry; use super::state::AdminRegistry;
use log::warn; use log::{debug, warn};
use serde_json::{json, Value}; use serde_json::{json, Value};
struct MyStaticRepository { struct MyStaticRepository {
/// In Memory Storage for Example.
content: Vec<Value>, content: Vec<Value>,
/// ID Counter for content.
next_id: usize,
} }
impl MyStaticRepository { impl MyStaticRepository {
pub fn new() -> Self { pub fn new() -> Self {
MyStaticRepository { MyStaticRepository {
next_id: 12,
content: vec![ content: vec![
json!({"id": 1, "name": "Strange", "age": 150, "level": "master" }), json!({"id": 1, "name": "Strange", "age": 150, "level": "master" }),
json!({"id": 2, "name": "Adam", "age": 12, "powers": 8}), json!({"id": 2, "name": "Adam", "age": 12, "powers": 8}),
@ -48,7 +52,6 @@ impl AdminRepository for MyStaticRepository {
} }
fn list(&self, model: &AdminModel) -> RepositoryList { fn list(&self, model: &AdminModel) -> RepositoryList {
// Admin needs to inject info in these.
RepositoryList::List { RepositoryList::List {
values: self values: self
.content .content
@ -66,28 +69,87 @@ impl AdminRepository for MyStaticRepository {
} }
} }
fn info(&self, model: &AdminModel) -> RepositoryInfo { fn info(&self, _model: &AdminModel) -> RepositoryInfo {
RepoInfo { RepoInfo {
name: "My Static Repository", name: "My Static Repository",
lookup_key: "id", lookup_key: "id",
display_list: &["id", "name", "level", "age"], display_list: &["id", "name", "level", "age"],
fields: &["name", "level", "age", "powers"],
} }
.into() .into()
} }
fn create(&self, model: &AdminModel, data: Value) -> Option<RepositoryItem> { fn create(&mut self, model: &AdminModel, mut data: Value) -> Option<RepositoryItem> {
println!("I would now create: {}", data); debug!("Asked to create: {}", data);
let new_id = self.next_id;
self.next_id += 1;
data["id"] = Value::Number(new_id.into());
debug!("I create: {}", data);
// Push the data into the repository
self.content.push(data.clone());
// Return the newly created item
Some(RepositoryItem {
detail_url: Some(format!("{}/detail/{}", model.admin_url, new_id)),
fields: data,
})
}
fn update(&mut self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem> {
debug!("I would now update: {}, {}", id, data);
// First, find the index of the item to update
let item_index = self.content.iter().position(|item| {
if let Some(item_id) = item.get("id") {
match id {
LookupKey::Integer(i) => item_id == &i,
LookupKey::String(ref s) => item_id == s,
}
} else {
false
}
});
// Then, update the item if found
if let Some(index) = item_index {
let item = &mut self.content[index];
*item = data.clone();
if let Some(item_id) = item.get("id") {
return Some(RepositoryItem {
detail_url: Some(format!("{}/detail/{}", model.admin_url, item_id)),
fields: data,
});
}
}
None None
} }
fn update(&self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem> { fn delete(&mut self, _model: &AdminModel, id: LookupKey) -> Option<Value> {
println!("I would now update: {}, {}", id, data); debug!("Would delete: {}", id);
let item_index = self.content.iter().position(|item| {
if let Some(item_id) = item.get("id") {
match id {
LookupKey::Integer(i) => item_id == &i,
LookupKey::String(ref s) => item_id == s,
}
} else {
false
}
});
if let Some(index) = item_index {
// Remove and return the item
Some(self.content.remove(index))
} else {
None None
} }
fn delete(&self, model: &AdminModel, id: LookupKey) -> Option<Value> {
println!("Would delete: {}", id);
None
} }
} }

View File

@ -1,3 +1,5 @@
use tokio::sync::Mutex;
use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository}; use super::domain::{AdminApp, AdminModel, AdminModelConfig, AdminRepository};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@ -9,7 +11,7 @@ pub struct AdminRegistry {
base_path: String, base_path: String,
apps: HashMap<String, internal::AdminApp>, apps: HashMap<String, internal::AdminApp>,
models: HashMap<String, internal::AdminModel>, models: HashMap<String, internal::AdminModel>,
repositories: HashMap<String, Box<dyn AdminRepository>>, repositories: HashMap<String, Arc<Mutex<dyn AdminRepository>>>,
} }
impl AdminRegistry { impl AdminRegistry {
@ -105,7 +107,8 @@ impl AdminRegistry {
repository: R, repository: R,
) -> Result<(), String> { ) -> Result<(), String> {
let model_key = self.register_model_config(model)?; let model_key = self.register_model_config(model)?;
self.repositories.insert(model_key, Box::new(repository)); self.repositories
.insert(model_key, Arc::new(Mutex::new(repository)));
Ok(()) Ok(())
} }
@ -113,10 +116,11 @@ impl AdminRegistry {
&self, &self,
app_key: &str, app_key: &str,
model_key: &str, model_key: &str,
) -> Result<&Box<dyn AdminRepository>, String> { ) -> Result<Arc<Mutex<dyn AdminRepository>>, String> {
let full_model_key = format!("{}.{}", app_key, model_key); let full_model_key = format!("{}.{}", app_key, model_key);
if let Some(repo) = self.repositories.get(&full_model_key) { if let Some(repo) = self.repositories.get(&full_model_key) {
return Ok(repo); // Clone the Arc to return a reference to the repository
return Ok(Arc::clone(repo));
} else { } else {
return Err("Couldn't find repository".to_owned()); return Err("Couldn't find repository".to_owned());
} }

View File

@ -114,6 +114,7 @@ pub async fn list_item_collection(
info!("list_item_collection {} for model {}", app_key, model_key); info!("list_item_collection {} for model {}", app_key, model_key);
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let repo = repo.lock().await;
let admin_model = registry let admin_model = registry
.get_model(&app_key, &model_key) .get_model(&app_key, &model_key)
.expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist .expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
@ -152,6 +153,7 @@ pub async fn item_details(
Path((app_key, model_key, id)): Path<(String, String, String)>, Path((app_key, model_key, id)): Path<(String, String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let repo = repo.lock().await;
let admin_model = registry let admin_model = registry
.get_model(&app_key, &model_key) .get_model(&app_key, &model_key)
.expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist .expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
@ -184,6 +186,7 @@ pub async fn new_item(
Path((app_key, model_key)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let repo = repo.lock().await;
let admin_model = registry let admin_model = registry
.get_model(&app_key, &model_key) .get_model(&app_key, &model_key)
.expect("Admin Model not found?"); .expect("Admin Model not found?");
@ -213,6 +216,7 @@ pub async fn create_item(
Form(form): Form<Value>, Form(form): Form<Value>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) { let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let mut repo = repo.lock().await;
let admin_model = registry let admin_model = registry
.get_model(&app_key, &model_key) .get_model(&app_key, &model_key)
.expect("Admin Model not found?"); .expect("Admin Model not found?");

View File

@ -1,17 +1,28 @@
{% extends base|none("admin/base.jinja") %} {% extends base|none("admin/base.jinja") %}
{% macro input(name, value="", type="text") -%} {% macro input(name, value="", type="text") -%}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}"> <input type="{{ type }}" name="{{ name }}" value="{{ value }}" placeholder="">
{%- endmacro %} {%- endmacro %}
{% block content %} {% block content %}
Create {{item_model.name}} in {{item_info.name}} <div class="ui container">
<h1>Create {{item_model.name}} in {{item_info.name}}</h1>
<form action="{{item_model.add_url}}" method="POST"> <form action="{{item_model.add_url}}" method="POST" class="ui form">
{% set fields = item_info.display_list %} {% set fields = item_info.fields %}
{% for field in fields %} {% for field in fields %}
<p><label>{{field}}</label>{{ input(field) }}</p> {% if item %}
{% set field_value = item.fields[field]|none("") %}
{% else %}
{% set field_value = "" %}
{% endif %}
<div class="inline field">
<label>{{field|capitalize}}</label>
{{ input(field, field_value) }}
</div>
{% endfor %} {% endfor %}
<input type="submit" name="submit" value="Create"> <button class="ui button" type="submit">Create</button>
</form> </form>
</div>
{% endblock content %} {% endblock content %}