BATMAN 🦇
This commit is contained in:
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod service;
|
||||
pub mod state;
|
||||
40
src/main.rs
Normal file
40
src/main.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
mod service;
|
||||
mod state;
|
||||
|
||||
use crate::service::{handlers, templates};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||
templates.render_html("index.html", ())
|
||||
}
|
||||
|
||||
async fn hello_world(templates: State<templates::Templates>) -> impl IntoResponse {
|
||||
templates.render_html("hello_world.html", ())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Prepare App State
|
||||
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
|
||||
let state: AppState = AppState { templates: tmpl };
|
||||
|
||||
// Application Route
|
||||
let app = Router::new()
|
||||
.route("/", get(home))
|
||||
.route("/hello", get(hello_world))
|
||||
.route_service("/static/*file", handlers::static_handler.into_service())
|
||||
.fallback(handlers::not_found_handler)
|
||||
.with_state(state);
|
||||
|
||||
// Run Server
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
println!("listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
86
src/service/error.rs
Normal file
86
src/service/error.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
pub struct Error {
|
||||
inner: anyhow::Error,
|
||||
status: StatusCode,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
// Provide a builder method to set the status code
|
||||
pub fn with_status(mut self, status: StatusCode) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
// Provide a builder method to set the custom message
|
||||
pub fn with_message(mut self, message: String) -> Self {
|
||||
self.message = message;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Error {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: anyhow::Error::new(std::fmt::Error),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: String::from("An internal error occurred"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> Response {
|
||||
(self.status, format!("{}: {}", self.message, self.inner)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}: {}", self.message, self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
// make it possible to use ? on anyhow::Error Results
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self {
|
||||
inner: err,
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: String::from("An unknown internal error occurred"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<minijinja::Error> for Error {
|
||||
fn from(err: minijinja::Error) -> Self {
|
||||
Self {
|
||||
inner: anyhow::Error::new(err),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: String::from("A Templating Error occured"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::anyhow;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
#[test]
|
||||
fn test_error_with_status_and_message() {
|
||||
let error = Error::from(anyhow!("Test error"))
|
||||
.with_status(StatusCode::BAD_REQUEST)
|
||||
.with_message(String::from("A custom error message"));
|
||||
|
||||
assert_eq!(error.status, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(error.message, "A custom error message");
|
||||
assert_eq!(format!("{}", error.inner), "Test error");
|
||||
}
|
||||
}
|
||||
73
src/service/handlers.rs
Normal file
73
src/service/handlers.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
/* Default handlers */
|
||||
use crate::service::templates::Templates;
|
||||
use axum::{
|
||||
body::{boxed, Full},
|
||||
extract::State,
|
||||
http::{header, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use rust_embed::RustEmbed;
|
||||
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)
|
||||
pub async fn not_found_handler(
|
||||
State(templates): State<Templates>,
|
||||
) -> axum::response::Result<impl IntoResponse> {
|
||||
not_found(templates)
|
||||
}
|
||||
|
||||
fn not_found(templates: Templates) -> axum::response::Result<impl IntoResponse> {
|
||||
let body = templates.render_html("http404.html", ());
|
||||
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 = boxed(Full::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>;
|
||||
3
src/service/mod.rs
Normal file
3
src/service/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod error;
|
||||
pub mod handlers;
|
||||
pub mod templates;
|
||||
71
src/service/templates.rs
Normal file
71
src/service/templates.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use crate::service::error::Error;
|
||||
use axum::response::Html;
|
||||
use minijinja::{path_loader, Environment, Template, Value};
|
||||
use minijinja_autoreload::AutoReloader;
|
||||
use pulldown_cmark::{Event, Tag};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct RenderTarget {}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Templates {
|
||||
pub reloader: Arc<minijinja_autoreload::AutoReloader>,
|
||||
}
|
||||
|
||||
impl Templates {
|
||||
pub fn initialize() -> Result<Self, String> {
|
||||
let reloader = AutoReloader::new(move |notifier| {
|
||||
let mut environment = Environment::new();
|
||||
let template_path = "templates";
|
||||
//environment.set_loader();
|
||||
|
||||
environment.set_loader(path_loader(&template_path));
|
||||
environment.add_filter("markdown", markdown);
|
||||
|
||||
notifier.watch_path(template_path, true);
|
||||
//environment.set_source(Source::from_path(template_path));
|
||||
Ok(environment)
|
||||
});
|
||||
Ok(Self {
|
||||
reloader: Arc::new(reloader),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render<D: serde::Serialize>(&self, key: &str, data: D) -> Result<String, Error> {
|
||||
let env = self.reloader.acquire_env()?;
|
||||
let template = env.get_template(key)?;
|
||||
let rendered = template.render(&data)?;
|
||||
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
pub fn render_html<D: serde::Serialize>(
|
||||
&self,
|
||||
key: &str,
|
||||
data: D,
|
||||
) -> Result<Html<String>, Error> {
|
||||
let result = self.render(key, data)?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
pub fn render_indirect(&self, key: &str) -> Result<RenderTarget, Error> {
|
||||
let env = self.reloader.acquire_env()?;
|
||||
|
||||
Ok(RenderTarget {})
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown(value: String) -> Value {
|
||||
let parser = pulldown_cmark::Parser::new(&value);
|
||||
let parser: Box<dyn Iterator<Item = Event>> = Box::new(parser.into_iter());
|
||||
let mut html = String::new();
|
||||
pulldown_cmark::html::push_html(&mut html, parser);
|
||||
Value::from_safe_string(html)
|
||||
/*
|
||||
value
|
||||
.to_lowercase()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join("-")
|
||||
*/
|
||||
}
|
||||
14
src/state.rs
Normal file
14
src/state.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use axum::extract::FromRef;
|
||||
|
||||
use crate::service::templates;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub templates: templates::Templates,
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for templates::Templates {
|
||||
fn from_ref(app_state: &AppState) -> templates::Templates {
|
||||
app_state.templates.clone()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user