feature: extracting admin and services to

This commit is contained in:
Gabor Körber 2024-02-28 14:01:09 +01:00
parent 7fe7047485
commit ac9488b299
9 changed files with 130 additions and 93 deletions

View File

@ -1 +1,4 @@
APP_HOST=127.0.0.1
APP_PORT=3000
DATABASE_URL=postgresql://miniweb:miniweb@localhost:54321/miniweb DATABASE_URL=postgresql://miniweb:miniweb@localhost:54321/miniweb
RUST_LOG=info

View File

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

View File

@ -9,6 +9,7 @@ use serde_json::Value;
use crate::admin::domain::{AdminApp, AdminModel}; use crate::admin::domain::{AdminApp, AdminModel};
use crate::admin::state; use crate::admin::state;
use crate::admin::state::AdminState;
use crate::service::templates; use crate::service::templates;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -76,11 +77,12 @@ pub fn base_template(headers: &HeaderMap) -> Option<String> {
} }
} }
pub async fn index( pub async fn index<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
) -> impl IntoResponse { ) -> impl IntoResponse {
let templates = admin.get_templates();
let registry = admin.get_registry();
templates.render_html( templates.render_html(
"admin/index.html", "admin/index.html",
AdminContext { AdminContext {
@ -92,26 +94,29 @@ pub async fn index(
} }
// Index Action is POST to the index site. We can anchor some general business code here. // Index Action is POST to the index site. We can anchor some general business code here.
pub async fn index_action() -> impl IntoResponse { pub async fn index_action<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
) -> impl IntoResponse {
"There is your answer!".to_owned() "There is your answer!".to_owned()
} }
pub async fn list_app( pub async fn list_app<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
Path(app_key): Path<String>, Path(app_key): Path<String>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let templates = admin.get_templates();
templates.render_html("admin/app_list.jinja", ()) templates.render_html("admin/app_list.jinja", ())
} }
// List Items renders the entire list item page. // List Items renders the entire list item page.
pub async fn list_item_collection( pub async fn list_item_collection<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
info!("list_item_collection {} for model {}", app_key, model_key); 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 context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let repo = repo.lock().await; let repo = repo.lock().await;
let admin_model = registry let admin_model = registry
@ -136,19 +141,21 @@ pub async fn list_item_collection(
} }
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input. // 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( pub async fn item_collection_action<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
Path((app_key, model_key)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
"There is your answer!".to_owned() "There is your answer!".to_owned()
} }
// Item Details shows one single dataset. // Item Details shows one single dataset.
pub async fn view_item_details( pub async fn view_item_details<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
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 templates = admin.get_templates();
let registry = admin.get_registry();
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 repo = repo.lock().await;
let admin_model = registry let admin_model = registry
@ -174,12 +181,13 @@ pub async fn view_item_details(
templates.render_html("admin/items/item_detail.jinja", context) templates.render_html("admin/items/item_detail.jinja", context)
} }
pub async fn new_item( pub async fn new_item<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse { ) -> 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 context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let repo = repo.lock().await; let repo = repo.lock().await;
let admin_model = registry let admin_model = registry
@ -203,13 +211,14 @@ pub async fn new_item(
templates.render_html("admin/items/item_create.jinja", context) templates.render_html("admin/items/item_create.jinja", context)
} }
pub async fn create_item( pub async fn create_item<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
Path((app_key, model_key)): Path<(String, String)>, Path((app_key, model_key)): Path<(String, String)>,
Form(form): Form<Value>, Form(form): Form<Value>,
) -> impl IntoResponse { ) -> 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 context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let mut repo = repo.lock().await; let mut repo = repo.lock().await;
let admin_model = registry let admin_model = registry
@ -240,12 +249,13 @@ pub async fn create_item(
} }
/// Change is the GET version. /// Change is the GET version.
pub async fn change_item( pub async fn change_item<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
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 templates = admin.get_templates();
let registry = admin.get_registry();
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 repo = repo.lock().await;
let admin_model = registry let admin_model = registry
@ -271,13 +281,14 @@ pub async fn change_item(
templates.render_html("admin/items/item_change.jinja", context) templates.render_html("admin/items/item_change.jinja", context)
} }
pub async fn update_item( pub async fn update_item<S: AdminState + Clone + Send + Sync + 'static>(
templates: State<templates::Templates>, admin: State<S>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap, headers: HeaderMap,
Path((app_key, model_key, id)): Path<(String, String, String)>, Path((app_key, model_key, id)): Path<(String, String, String)>,
Form(form): Form<Value>, Form(form): Form<Value>,
) -> impl IntoResponse { ) -> 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 context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let mut repo = repo.lock().await; let mut repo = repo.lock().await;
let admin_model = registry let admin_model = registry
@ -307,13 +318,17 @@ pub async fn update_item(
} }
// Item Action allows running an action on one single dataset. // Item Action allows running an action on one single dataset.
pub async fn item_action( 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)>, Path((app_key, model_key, model_id)): Path<(String, String, String)>,
) -> impl IntoResponse { ) -> impl IntoResponse {
"There is your answer!".to_owned() "There is your answer!".to_owned()
} }
pub async fn debug_view(Path(data): Path<String>) -> impl IntoResponse { pub async fn debug_view<S: AdminState + Clone + Send + Sync + 'static>(
admin: State<S>,
Path(data): Path<String>,
) -> impl IntoResponse {
println!("debug: {}", data); println!("debug: {}", data);
"Debug!".to_owned() "Debug!".to_owned()
} }

View File

@ -9,17 +9,6 @@ use axum::{
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::marker::PhantomData; use std::marker::PhantomData;
// usage: .route_service("/static/*file", static_handler.into_service())
pub async fn static_handler(uri: Uri) -> impl IntoResponse {
let mut path = uri.path().trim_start_matches('/').to_string();
if path.starts_with("static/") {
path = path.replace("static/", "");
}
StaticFile::get(path)
}
// usage: .fallback(not_found_handler) // usage: .fallback(not_found_handler)
pub async fn not_found_handler( pub async fn not_found_handler(
State(templates): State<Templates>, State(templates): State<Templates>,
@ -31,43 +20,3 @@ fn not_found(templates: Templates) -> axum::response::Result<impl IntoResponse>
let body = templates.render_html("http404.html", ()); let body = templates.render_html("http404.html", ());
Ok((StatusCode::NOT_FOUND, body)) Ok((StatusCode::NOT_FOUND, body))
} }
pub struct EmbeddedFile<E, T> {
pub path: T,
embed: PhantomData<E>,
}
impl<E, T> EmbeddedFile<E, T> {
pub fn get(path: T) -> Self {
Self {
path,
embed: PhantomData,
}
}
}
impl<E, T> IntoResponse for EmbeddedFile<E, T>
where
E: RustEmbed,
T: AsRef<str>,
{
fn into_response(self) -> Response {
let path: &str = self.path.as_ref();
match E::get(path) {
Some(content) => {
let body = Body::from(content.data);
let mime = mime_guess::from_path(path).first_or_octet_stream();
Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.body(body)
.unwrap()
}
None => StatusCode::NOT_FOUND.into_response(),
}
}
}
#[derive(RustEmbed)]
#[folder = "static"]
struct StaticDir;
type StaticFile<T> = EmbeddedFile<StaticDir, T>;

58
src/embed.rs Normal file
View File

@ -0,0 +1,58 @@
use axum::{
body::Body,
http::{header, StatusCode, Uri},
response::{IntoResponse, Response},
};
use rust_embed::RustEmbed;
use std::marker::PhantomData;
pub struct EmbeddedFile<E, T> {
pub path: T,
embed: PhantomData<E>,
}
impl<E, T> EmbeddedFile<E, T> {
pub fn get(path: T) -> Self {
Self {
path,
embed: PhantomData,
}
}
}
impl<E, T> IntoResponse for EmbeddedFile<E, T>
where
E: RustEmbed,
T: AsRef<str>,
{
fn into_response(self) -> Response {
let path: &str = self.path.as_ref();
match E::get(path) {
Some(content) => {
let body = Body::from(content.data);
let mime = mime_guess::from_path(path).first_or_octet_stream();
Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.body(body)
.unwrap()
}
None => StatusCode::NOT_FOUND.into_response(),
}
}
}
#[derive(RustEmbed)]
#[folder = "static"]
struct StaticDir;
type StaticFile<T> = EmbeddedFile<StaticDir, T>;
// usage: .route_service("/static/*file", static_handler.into_service())
pub async fn static_handler(uri: Uri) -> impl IntoResponse {
let mut path = uri.path().trim_start_matches('/').to_string();
if path.starts_with("static/") {
path = path.replace("static/", "");
}
StaticFile::get(path)
}

View File

@ -1,6 +1,6 @@
use axum::{extract::State, response::IntoResponse, Form}; use axum::{extract::State, response::IntoResponse, Form};
use crate::service::templates; use rear::service::templates;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@ -1,4 +1 @@
pub mod admin;
pub mod auth;
pub mod service;
pub mod state; pub mod state;

View File

@ -1,4 +1,5 @@
mod admin_examples; mod admin_examples;
mod embed;
mod howto; mod howto;
mod state; mod state;
@ -16,6 +17,7 @@ use axum::{
}; };
use dotenvy::dotenv; use dotenvy::dotenv;
use log::info; use log::info;
use rear::admin;
use rear::service::{handlers, templates}; use rear::service::{handlers, templates};
use std::env; use std::env;
use std::net::SocketAddr; use std::net::SocketAddr;
@ -66,7 +68,7 @@ async fn main() {
//.merge(admin_router) //.merge(admin_router)
.nest("/admin", admin::routes()) .nest("/admin", admin::routes())
.nest("/howto", howto::routes()) .nest("/howto", howto::routes())
.route_service("/static/*file", handlers::static_handler.into_service()) .route_service("/static/*file", embed::static_handler.into_service())
.fallback(handlers::not_found_handler) .fallback(handlers::not_found_handler)
.with_state(state) .with_state(state)
.layer( .layer(

View File

@ -1,12 +1,12 @@
use axum::extract::FromRef; use axum::extract::FromRef;
use rear::admin::state::SharedRegistry; use rear::admin::state::{AdminState, SharedRegistry};
use rear::service::templates; use rear::service::templates;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub templates: templates::Templates, pub templates: templates::Templates,
pub admin: AdminState, pub admin: SharedRegistry,
} }
impl FromRef<AppState> for templates::Templates { impl FromRef<AppState> for templates::Templates {
@ -20,3 +20,13 @@ impl FromRef<AppState> for SharedRegistry {
app_state.admin.clone() app_state.admin.clone()
} }
} }
impl AdminState for AppState {
fn get_templates(&self) -> &templates::Templates {
&self.templates
}
fn get_registry(&self) -> SharedRegistry {
self.admin.clone()
}
}