use crate::api::v1;
use crate::{AdminPanel, RenderError, RenderedPage};
use async_trait::async_trait;
use axum::extract::{Path, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response};
use axum::{routing, Form, Json, Router};
use feattle_core::{Feattles, UpdateError};
use serde::Deserialize;
use std::sync::Arc;
#[async_trait]
pub trait ExtractModifiedBy: Send + Sync + 'static {
async fn extract_modified_by(&self, headers: &HeaderMap) -> Result<String, Response>;
}
pub fn axum_router<F>(
admin_panel: Arc<AdminPanel<F>>,
extract_modified_by: impl ExtractModifiedBy,
) -> Router<()>
where
F: Feattles + Sync + Send + 'static,
{
async fn list_feattles<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
) -> impl IntoResponse {
state.admin_panel.list_feattles().await
}
async fn list_feattles_api_v1<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
) -> impl IntoResponse {
state.admin_panel.list_feattles_api_v1().await.map(Json)
}
async fn show_feattle<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
Path(key): Path<String>,
) -> impl IntoResponse {
state.admin_panel.show_feattle(&key).await
}
async fn show_feattle_api_v1<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
Path(key): Path<String>,
) -> impl IntoResponse {
state.admin_panel.show_feattle_api_v1(&key).await.map(Json)
}
async fn edit_feattle<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
Path(key): Path<String>,
headers: HeaderMap,
Form(form): Form<EditFeattleForm>,
) -> Response {
let modified_by = match state
.extract_modified_by
.extract_modified_by(&headers)
.await
{
Ok(modified_by) => modified_by,
Err(response) => return response,
};
state
.admin_panel
.edit_feattle(&key, &form.value_json, modified_by)
.await
.map(|_| Redirect::to("/"))
.into_response()
}
async fn edit_feattle_api_v1<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
Path(key): Path<String>,
Json(request): Json<v1::EditFeattleRequest>,
) -> impl IntoResponse {
state
.admin_panel
.edit_feattle_api_v1(&key, request)
.await
.map(Json)
}
async fn render_public_file<F: Feattles + Sync>(
State(state): State<RouterState<F>>,
Path(file_name): Path<String>,
) -> impl IntoResponse {
state.admin_panel.render_public_file(&file_name)
}
let state = RouterState {
admin_panel,
extract_modified_by: Arc::new(extract_modified_by),
};
Router::new()
.route("/", routing::get(list_feattles))
.route("/api/v1/feattles", routing::get(list_feattles_api_v1))
.route("/feattle/{key}", routing::get(show_feattle))
.route("/api/v1/feattle/{key}", routing::get(show_feattle_api_v1))
.route("/feattle/{key}/edit", routing::post(edit_feattle))
.route("/api/v1/feattle/{key}", routing::post(edit_feattle_api_v1))
.route("/public/{file_name}", routing::get(render_public_file))
.with_state(state)
}
#[derive(Debug, Deserialize)]
struct EditFeattleForm {
value_json: String,
}
struct RouterState<F> {
admin_panel: Arc<AdminPanel<F>>,
extract_modified_by: Arc<dyn ExtractModifiedBy>,
}
impl IntoResponse for RenderedPage {
fn into_response(self) -> Response {
([("Content-Type", self.content_type)], self.content).into_response()
}
}
impl IntoResponse for RenderError {
fn into_response(self) -> Response {
match self {
RenderError::NotFound | RenderError::Update(UpdateError::UnknownKey(_)) => {
StatusCode::NOT_FOUND.into_response()
}
RenderError::Update(UpdateError::Parsing(err)) => (
StatusCode::BAD_REQUEST,
format!("Failed to parse: {:?}", err),
)
.into_response(),
err => {
log::error!("request failed with {:?}", err);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", err)).into_response()
}
}
}
}
impl<F> Clone for RouterState<F> {
fn clone(&self) -> Self {
RouterState {
admin_panel: self.admin_panel.clone(),
extract_modified_by: self.extract_modified_by.clone(),
}
}
}
#[async_trait]
impl ExtractModifiedBy for String {
async fn extract_modified_by(&self, _headers: &HeaderMap) -> Result<String, Response> {
Ok(self.clone())
}
}
#[async_trait]
impl ExtractModifiedBy for &'static str {
async fn extract_modified_by(&self, _headers: &HeaderMap) -> Result<String, Response> {
Ok(self.to_string())
}
}
#[async_trait]
impl<F, R> ExtractModifiedBy for F
where
F: Fn(&HeaderMap) -> Result<String, R> + Send + Sync + 'static,
R: IntoResponse,
{
async fn extract_modified_by(&self, headers: &HeaderMap) -> Result<String, Response> {
self(headers).map_err(|response| response.into_response())
}
}