basic counter

This commit is contained in:
Victor Martinez 2023-07-19 02:18:28 +02:00
commit b81e82512d
12 changed files with 1357 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1135
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "axum-htmx"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.6.18"
config = "0.13.3"
serde = { version = "1.0.167", features = ["derive", "rc"] }
tokio = { version = "1.29.1", features = ["full"] }
anyhow = "1.0.71"
askama = "0.12.0"

10
Makefile.toml Normal file
View file

@ -0,0 +1,10 @@
[env]
PORT = "3000"
[tasks.run]
command = "cargo"
args = ["run"]
[tasks.run-watch]
command = "cargo"
args = ["watch", "-x", "run"]

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# axum-htmx
An example on how to easily serve an HTMX application using Axum and Askama

30
src/configuration.rs Normal file
View file

@ -0,0 +1,30 @@
use anyhow::Context;
use config::Config;
use serde::Deserialize;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[derive(Deserialize)]
pub struct Configuration {
pub port: u16,
}
impl Configuration {
pub fn load() -> anyhow::Result<Self> {
let config = Config::builder()
.add_source(
config::Environment::default()
.try_parsing(true)
.separator("__"),
)
.build()
.context("Failed to load app configuration")?
.try_deserialize()
.context("Cannot deserialize configuration")?;
Ok(config)
}
pub fn address(&self) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), self.port)
}
}

30
src/error.rs Normal file
View file

@ -0,0 +1,30 @@
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
// Make our own error that wraps `anyhow::Error`.
#[derive(Debug)]
pub struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal error: {self:#?}"),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

41
src/main.rs Normal file
View file

@ -0,0 +1,41 @@
mod configuration;
mod error;
mod pages;
use axum::{
routing::{get, post},
Router,
};
use configuration::Configuration;
use pages::counter::CounterState;
use std::sync::Arc;
struct AppState {
counter: Arc<CounterState>,
}
fn router(state: AppState) -> Router {
Router::new()
.route(
"/counter",
get(pages::counter::view).with_state(state.counter.clone()),
)
.route(
"/counter/increment",
post(pages::counter::increment).with_state(state.counter),
)
}
#[tokio::main]
async fn main() {
let config = Configuration::load().expect("Error loading configuration");
let address = config.address();
let state = AppState {
counter: Arc::new(CounterState::new()),
};
axum::Server::bind(&address)
.serve(router(state).into_make_service())
.await
.unwrap();
}

36
src/pages/counter.rs Normal file
View file

@ -0,0 +1,36 @@
use crate::pages::HtmlTemplate;
use askama::Template;
use axum::{
extract::State,
response::{Html, IntoResponse},
};
use std::sync::{atomic::AtomicUsize, Arc};
pub struct CounterState(AtomicUsize);
impl CounterState {
pub fn new() -> Self {
CounterState(AtomicUsize::new(0))
}
}
#[derive(Template)]
#[template(path = "counter.html")]
pub struct CounterTemplate {
pub count: usize,
}
pub async fn view(State(state): State<Arc<CounterState>>) -> impl IntoResponse {
let template = CounterTemplate {
count: state.0.load(std::sync::atomic::Ordering::Relaxed),
};
HtmlTemplate::new(template)
}
pub async fn increment(State(state): State<Arc<CounterState>>) -> impl IntoResponse {
let increment = 1;
let previous = state
.0
.fetch_add(increment, std::sync::atomic::Ordering::Relaxed);
Html(format!("{}", previous + increment))
}

View file

@ -0,0 +1,30 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse, Response},
};
pub struct HtmlTemplate<T>(T);
impl<T: Template> HtmlTemplate<T> {
pub fn new(template: T) -> Self {
HtmlTemplate(template)
}
}
impl<T> IntoResponse for HtmlTemplate<T>
where
T: Template,
{
fn into_response(self) -> Response {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {}", err),
)
.into_response(),
}
}
}

4
src/pages/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod counter;
mod html_template;
pub use html_template::*;

23
templates/counter.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Counter</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- <link href="css/style.css" rel="stylesheet"> -->
<script src="https://unpkg.com/htmx.org@1.9.3"
integrity="sha384-lVb3Rd/Ca0AxaoZg5sACe8FJKF0tnUgR2Kd7ehUOG5GCcROv5uBIZsOqovBAcWua"
crossorigin="anonymous"></script>
</head>
<body>
<div>
<button hx-post="/counter/increment" hx-trigger="click" hx-target="#count">
Click Me!
</button>
<div id="count">{{ count }}</div>
</div>
</body>
</html>