Skip to content

Commit

Permalink
Merge pull request #9 from husni-zuhdi/test-axum-framework
Browse files Browse the repository at this point in the history
Test axum framework
  • Loading branch information
husni-zuhdi authored Aug 22, 2024
2 parents e657913 + 6da32af commit bc82045
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 101 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.73-buster AS builder
FROM rust:1.74-buster AS builder
COPY . .
RUN cargo build --release

Expand Down
6 changes: 3 additions & 3 deletions cmd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[package]
name = "cmd"
version = "0.1.2"
version = "0.1.3"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
internal = { path = "../internal", version = "0.1.2"}
actix-web = "4.4.0"
internal = { path = "../internal", version = "0.1.3"}
tokio = { version = "1.0", features = ["full"] }
5 changes: 3 additions & 2 deletions cmd/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use internal::{self, config::Config, handler::handler};

#[actix_web::main]
#[tokio::main]
async fn main() -> std::io::Result<()> {
let config = Config::from_envar();
handler(config).await
handler(config).await;
Ok(())
}
14 changes: 8 additions & 6 deletions internal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
[package]
name = "internal"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
build = "build.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "4.4.0"
actix-web-lab = "0.20.0"
actix-files = "0.6.5"
axum = "0.7.5"
tokio = { version = "1.0", features = ["full"] }
# tracing = "0.1" # Might not need this
# tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Might not need this
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.5.0", features = ["fs", "trace"] }
askama = { version = "0.12.1"}
env_logger = "0.10.1"
log = "0.4.20"
Expand All @@ -23,5 +26,4 @@ regex = "1.10.6"

[build-dependencies]
anyhow = "1.0.86"
# Minimal vergen
vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] }
vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] } #Minimal Vergen
59 changes: 30 additions & 29 deletions internal/src/handler.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
use crate::config::Config;
use crate::model::data::BlogsData;
use crate::model::data::{AppState, BlogsData};
use crate::router::*;
use actix_web::{middleware, web, App, HttpServer};
use axum::{
routing::{get, get_service},
Router,
};
use log::info;
use tower_http::services::{ServeDir, ServeFile};

pub async fn handler(cfg: Config) -> std::io::Result<()> {
pub async fn handler(cfg: Config) -> () {
// Initialize Logger
env_logger::init_from_env(env_logger::Env::new().default_filter_or(cfg.log_level.clone()));

let endpoint = cfg.svc_endpoint.as_str();
let port = cfg
.svc_port
.parse::<u16>()
.expect("Failed to get port number");

// Setup config and blogs_data states
let config = cfg.clone();
let mut blogs_data = BlogsData::default();
if !config.gh_owner.is_empty() && !config.gh_repo.is_empty() && !config.gh_branch.is_empty() {
blogs_data = BlogsData::with_gh(&config.gh_owner, &config.gh_repo, &config.gh_branch).await;
}
let app_state = AppState { config, blogs_data };

let endpoint = format!("{}:{}", cfg.svc_endpoint, cfg.svc_port);
info!("Starting HTTP Server at http://{}", endpoint);

info!(
"Starting HTTP Server at http://{}:{}",
cfg.svc_endpoint, cfg.svc_port
);
// Axum Application
let app = Router::new()
.route("/", get(get_profile))
.route("/not-found", get(get_404_not_found))
.route("/version", get(get_version))
.route("/blogs", get(get_blogs))
.route("/blogs/:blog_id", get(get_blog))
.nest_service("/statics", get_service(ServeDir::new("./statics/favicon/")))
.nest_service(
"/statics/styles.css",
get_service(ServeFile::new("./statics/styles.css")),
)
.with_state(app_state)
.fallback(get(get_404_not_found));

HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
.app_data(web::Data::new(blogs_data.clone()))
.app_data(web::Data::new(config.clone()))
.service(web::resource("/").route(web::get().to(profile)))
.service(web::resource("/statics/{static_file}").route(web::get().to(statics)))
.service(web::resource("/blogs").route(web::get().to(get_blogs)))
.service(web::resource("/blogs/{blogid}").route(web::get().to(get_blog)))
.service(web::resource("/version").route(web::get().to(get_version)))
.service(web::resource("/not-found").route(web::get().to(get_404_not_found)))
})
.bind((endpoint, port))
.expect("Failed to start Http Server")
.run()
.await
// Start Axum Application
let listener = tokio::net::TcpListener::bind(endpoint).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
9 changes: 9 additions & 0 deletions internal/src/model/data.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::api::github::get_gh_blog_data;
use crate::config::Config;
use crate::utils::{capitalize, md_to_html};
use log::{debug, info};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -158,6 +159,14 @@ pub struct Tree {
pub url: String,
}

// Axum state
// Consist of Config and BlogsData
#[derive(Debug, Clone)]
pub struct AppState {
pub config: Config,
pub blogs_data: BlogsData,
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
6 changes: 5 additions & 1 deletion internal/src/model/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,9 @@ pub struct Version<'a> {
}

#[derive(Template, Debug)]
#[template(path = "not_found.html")]
#[template(path = "404_not_found.html")]
pub struct NotFound;

#[derive(Template, Debug)]
#[template(path = "500_internal_server_error.html")]
pub struct InternalServerError;
173 changes: 117 additions & 56 deletions internal/src/router.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
use crate::config::Config;
use crate::model::{data::*, templates::*};
use crate::utils::read_version_manifest;
use actix_files::NamedFile;
use actix_web::{web, Responder, Result};
use actix_web_lab::respond::Html;
use askama::Template;
use log::{error, info};
use axum::extract::{Path, State};
use axum::response::Html;
use log::{debug, error, info, warn};

pub async fn statics(path: web::Path<String>) -> Result<impl Responder> {
info!("Statics path: {}", path.clone());
let static_path = match path.into_inner().as_str() {
"styles.css" => Ok(format!("./statics/styles.css")),
"apple-touch-icon.png" => Ok(format!("./statics/favicon/apple-touch-icon.png")),
"favicon-16x16.png" => Ok(format!("./statics/favicon/favicon-16x16.png")),
"favicon-32x32.png" => Ok(format!("./statics/favicon/favicon-32x32.png")),
_ => {
let err = "Failed to get static path. Statics path is not allowed.";
error!("{}", err);
Err(err)
/// Note: In axum [example](https://docs.rs/axum/latest/axum/response/index.html#building-responses)
/// They show an example to return Html<&'static str>
/// Instaed of Html<String>. But using static give me a headache :")
/// get_profile
/// Serve Profile/Biography HTML file
pub async fn get_profile() -> Html<String> {
let profile = Profile.render();
match profile {
Ok(res) => {
info!("Profile askama template rendered.");
Html(res)
}
Err(err) => {
error!("Failed to render profile.html. {}", err);
get_500_internal_server_error()
}
}
.expect("Failed to get static path");
let static_file = NamedFile::open(static_path).expect("Failed to render static file(s)");
Ok(static_file)
}

pub async fn profile() -> Result<impl Responder> {
let profile = Profile.render().expect("Failed to render profile.html");
Ok(Html(profile))
}

pub async fn get_404_not_found() -> Result<impl Responder> {
let html = NotFound.render().expect("Failed to render not_found.html");
Ok(Html(html))
}

pub async fn get_blogs(blogs_data: web::Data<BlogsData>) -> Result<impl Responder> {
/// get_blogs
/// Serve get_blogs HTML file
/// List our blogs title and id
pub async fn get_blogs(State(app_state): State<AppState>) -> Html<String> {
// Copy data to Template struct
let blogs_template: Vec<Blog> = blogs_data
let blogs: Vec<Blog> = app_state
.blogs_data
.blogs
.iter()
.map(|blog| Blog {
Expand All @@ -47,50 +41,117 @@ pub async fn get_blogs(blogs_data: web::Data<BlogsData>) -> Result<impl Responde
body: &blog.body,
})
.collect();
debug!("Blogs: {:?}", &blogs);

let blogs = Blogs {
blogs: &blogs_template,
let blogs_res = Blogs { blogs: &blogs }.render();
match blogs_res {
Ok(res) => {
info!("Blogs askama template rendered.");
Html(res)
}
Err(err) => {
error!("Failed to render get_blogs.html. {}", err);
get_500_internal_server_error()
}
}
.render()
.expect("Failed to render blogs.html");
info!("Blogs Template created");
Ok(Html(blogs))
}

pub async fn get_blog(
path: web::Path<String>,
blogs_data: web::Data<BlogsData>,
) -> Result<impl Responder> {
let blog_id = path.into_inner();
let blog_data = blogs_data
/// get_blog
/// Serve get_blog HTML file
/// Render our blog
pub async fn get_blog(Path(path): Path<String>, State(app_state): State<AppState>) -> Html<String> {
let state = app_state
.blogs_data
.blogs
.iter()
.filter(|blog| blog.id == blog_id)
.next()
.expect("Failed to get blog name with id {blog_id}");
.filter(|blog| &blog.id == &path)
.next();
debug!("BlogData: {:?}", &state);

match state {
Some(_) => {}
None => {
warn!(
"Failed to get blog with ID {}. Retunre 404 Not Found",
&path
);
return get_404_not_found().await;
}
}

let blog_data = state.unwrap();
let blog = Blog {
id: &blog_id,
id: path.clone().as_str(),
name: &blog_data.name,
filename: &blog_data.filename,
body: &blog_data.body,
}
.render()
.expect("Failed to render blog.html");
Ok(Html(blog))
.render();

match blog {
Ok(res) => {
info!("Blog ID {} askama template rendered.", &path);
Html(res)
}
Err(err) => {
error!("Failed to render blog.html. {}", err);
get_500_internal_server_error()
}
}
}

pub async fn get_version(config: web::Data<Config>) -> Result<impl Responder> {
/// get_version
/// Serve get_version HTML file
pub async fn get_version(State(app_state): State<AppState>) -> Html<String> {
let version_json = read_version_manifest().expect("Failed to get version manifest");
let version = Version {
version: version_json.version.as_str(),
environment: config.environment.as_str(),
environment: app_state.config.environment.as_str(),
build_hash: version_json.build_hash.as_str(),
build_date: version_json.build_date.as_str(),
}
.render()
.expect("Failed to render version.html");
info!("Version Template created");
.render();

match version {
Ok(res) => {
info!("Version askama template rendered.");
Html(res)
}
Err(err) => {
error!("Failed to render version.html. {}", err);
get_500_internal_server_error()
}
}
}

/// get_404_not_found
/// Serve 404 Not found HTML file
pub async fn get_404_not_found() -> Html<String> {
let not_found = NotFound.render();
match not_found {
Ok(res) => {
info!("NotFound askama template rendered.");
Html(res)
}
Err(err) => {
error!("Failed to render 404_not_found.html. {}", err);
get_500_internal_server_error()
}
}
}

Ok(Html(version))
/// get_500_internal_server_error
/// Serve 500 Internal Server Error HTML file
fn get_500_internal_server_error() -> Html<String> {
let internal_server_error = InternalServerError.render();
match internal_server_error {
Ok(res) => {
info!("InternalServerError askama template rendered.");
Html(res)
}
Err(err) => {
error!("Failed to render 500_internal_server_error.html. {}", err);
Html("We're fucked up.".to_string())
}
}
}
File renamed without changes.
Loading

0 comments on commit bc82045

Please sign in to comment.